Merge remote-tracking branch 'aosp/upstream-master' into merge

Test: None
Change-Id: I8a34a16c1ec7981a9cc93758e216ae54bac986f6
diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE
new file mode 100644
index 0000000..7d484c6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE
@@ -0,0 +1,16 @@
+Please answer these questions before submitting a bug report.
+
+### What version of OpenCensus are you using?
+
+
+### What JVM are you using (`java -version`)?
+
+
+### What did you do?
+If possible, provide a recipe for reproducing the error.
+
+
+### What did you expect to see?
+
+
+### What did you see instead?
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b5caa38
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,46 @@
+# Gradle
+build
+gradle.properties
+.gradle
+local.properties
+out/
+
+# Protobuf
+gen_gradle
+
+# Bazel
+bazel-*
+
+# Maven (proto)
+target
+
+# IntelliJ IDEA
+.idea
+*.iml
+.editorconfig
+
+# Eclipse
+.classpath
+.project
+.settings
+bin
+
+# NetBeans
+/.nb-gradle
+/.nb-gradle-properties
+
+# VS Code
+.vscode
+
+# OS X
+.DS_Store
+
+# Emacs
+*~
+\#*\#
+
+# Vim
+.swp
+
+# Other
+TAGS
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.gitmodules
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..996d4c0
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,90 @@
+sudo: false
+
+language: java
+
+matrix:
+  fast_finish: true
+  include:
+  - jdk: openjdk7
+    env: TASK=BUILD
+    os: linux
+
+  - jdk: oraclejdk8
+    env: TASK=BUILD
+    os: linux
+    addons:
+      apt:
+        packages:
+          # Install the JREs that are used for integration tests in
+          # contrib/agent, but are not installed by default.
+          - openjdk-6-jdk
+
+  # - jdk: oraclejdk9
+  #   env: TASK=BUILD
+  #   os: linux
+
+  - jdk: oraclejdk8
+    env: TASK=CHECKER_FRAMEWORK
+    os: linux
+
+  - env: TASK=CHECK_GIT_HISTORY
+    os: linux
+
+  # Build example projects last, since they are affected by fewer pull requests.
+  - jdk: oraclejdk8
+    env: TASK=CHECK_EXAMPLES_LICENSE
+    os: linux
+
+  - jdk: oraclejdk8
+    env: TASK=BUILD_EXAMPLES_GRADLE
+    os: linux
+
+  - jdk: oraclejdk8
+    env: TASK=BUILD_EXAMPLES_MAVEN
+    os: linux
+
+  - jdk: oraclejdk8
+    env: TASK=BUILD_EXAMPLES_BAZEL
+    os: linux
+
+  - jdk: oraclejdk8
+    env: TASK=CHECK_EXAMPLES_FORMAT
+    os: linux
+
+  # Work around https://github.com/travis-ci/travis-ci/issues/2317
+  - env: TASK=BUILD
+    os: osx
+
+  allow_failures:
+  # Allowing failures because osx builds are very slow.
+  - env: TASK=BUILD
+    os: osx
+
+before_install:
+  - git log --oneline --decorate --graph -30
+  - if \[ "$TASK" == "BUILD_EXAMPLES_BAZEL" \]; then
+      echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list ;
+      curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - ;
+      sudo apt-get update ;
+      sudo apt-get install bazel ;
+    fi
+
+# Skip Travis' default Gradle install step. See http://stackoverflow.com/a/26575080.
+install: true
+
+script:
+  - scripts/travis_script
+
+after_success:
+  - if \[ "$TASK" == "BUILD" \] && \[ "$TRAVIS_JDK_VERSION" == "oraclejdk8" \] && \[ "$TRAVIS_OS_NAME" = linux \]; then
+      bash <(curl -s https://codecov.io/bash) ;
+    fi
+
+before_cache:
+  - rm -fr $HOME/.gradle/caches/modules-2/modules-2.lock
+
+cache:
+  directories:
+    - $HOME/.gradle
+    - $HOME/.gradle/caches/
+    - $HOME/.gradle/wrapper/
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..e068e73
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Google Inc.
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..352c241
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,150 @@
+## Unreleased
+- Add `AttributeValueDouble` to `AttributeValue`.
+- Add `createWithSender` to `JaegerTraceExporter` to allow use of `HttpSender`
+  with extra configurations.
+- Add an API `Functions.returnToString()`.
+- Migrate to new Stackdriver Kubernetes monitored resource. This could be a breaking change
+  if you are using `gke_container` resources. For more info,
+  https://cloud.google.com/monitoring/kubernetes-engine/migration#incompatible
+- Add OpenCensus Java OC-Agent Trace Exporter.
+
+## 0.16.1 - 2018-09-18
+- Fix ClassCastException in Log4j log correlation
+  ([#1436](https://github.com/census-instrumentation/opencensus-java/issues/1436)).
+- Allow users to report metrics for their registered domain (using custom prefix). This could be a
+  breaking change if you have custom prefix without (registered) domain.
+
+## 0.16.0 - 2018-09-14
+- Add APIs to register gRPC client and server views separately.
+- Add an API MeasureMap.putAttachment() for recording exemplars.
+- Add Exemplar class and an API to get Exemplar list to DistributionData.
+- Improve the styling of Rpcz, Statsz, Tracez, and Traceconfigz pages.
+- Add an artifact `opencensus-contrib-exemplar-util` that has helper utilities 
+  on recording exemplars.
+- Reduce the default limit on `Link`s per `Span` to 32 (was 128 before).
+- Add Spring support for `@Traced` annotation and java.sql.PreparedStatements 
+  tracing.
+- Allow custom prefix for Stackdriver metrics in `StackdriverStatsConfiguration`.
+- Add support to handle the Tracestate in the SpanContext.
+- Remove global synchronization from the get current stats state.
+- Add get/from{Byte} methods on TraceOptions and deprecate get/from{Bytes}.
+- Add an API to `StackdriverTraceConfiguration` to allow setting a
+  `TraceServiceStub` instance to be used for export RPC calls.
+- Add an experimental artifact, `opencensus-contrib-log-correlation-log4j2`, for
+  adding tracing data to Log4j 2 LogEvents.
+
+## 0.15.1 - 2018-08-28
+- Improve propagation performance by avoiding doing string formatting when calling checkArgument.
+
+## 0.15.0 - 2018-06-20
+- Expose the factory methods of MonitoredResource.
+- Add an experimental artifact, `opencensus-contrib-log-correlation-stackdriver`, for
+  correlating traces and logs with Stackdriver Logging.
+
+## 0.14.0 - 2018-06-04
+- Adds Tracing.getExportComponent().shutdown() for use within application shutdown hooks.
+- `Duration.create` now throws an `IllegalArgumentException` instead of
+  returning a zero `Duration` when the arguments are invalid.
+- `Timestamp.create` now throws an `IllegalArgumentException` instead of
+  returning a zero `Timestamp` when the arguments are invalid.
+- Remove namespace and help message prefix for Prometheus exporter. This could be
+  a breaking change if you have Prometheus metrics from OpenCensus Prometheus exporter
+  of previous versions, please point to the new metrics with no namespace instead.
+- Add an util artifact `opencensus-contrib-appengine-standard-util` to interact with the AppEngine
+  CloudTraceContext.
+- Add support for Span kinds. (fix [#1054](https://github.com/census-instrumentation/opencensus-java/issues/1054)).
+- Add client/server started_rpcs measures and views to RPC constants.
+
+## 0.13.2 - 2018-05-08
+- Map http attributes to Stackdriver format (fix [#1153](https://github.com/census-instrumentation/opencensus-java/issues/1153)).
+
+## 0.13.1 - 2018-05-02
+- Fix a typo on displaying Aggregation Type for a View on StatsZ page.
+- Set bucket bounds as "le" labels for Prometheus Stats exporter.
+
+## 0.13.0 - 2018-04-27
+- Support building with Java 9.
+- Add a QuickStart example.
+- Remove extraneous dependencies from the Agent's `pom.xml`.
+- Deprecate `Window` and `WindowData`.
+- Add a configuration class to the Prometheus stats exporter.
+- Fix build on platforms that are not supported by `netty-tcnative`.
+- Add Jaeger trace exporter.
+- Add a gRPC Hello World example.
+- Remove usages of Guava collections in `opencensus-api`.
+- Set unit "1" when the aggregation type is Count.
+- Auto detect GCE and GKE Stackdriver MonitoredResources.
+- Make Error Prone and FindBugs annotations `compileOnly` dependencies.
+- Deprecate `Mean` and `MeanData`.
+- Sort `TagKey`s in `View.create(...)`.
+- Add utility class to expose default HTTP measures, tags and view, and register
+  default views.
+- Add new RPC measure and view constants, deprecate old ones.
+- Makes the trace and span ID fields mandatory in binary format.
+- Auto detect AWS EC2 resources.
+- Add `Duration.toMillis()`.
+- Make monitored resource utils a separate artifact `opencensus-contrib-monitored-resource-util`,
+  so that it can be reused across exporters.
+- Add `LastValue`, `LastValueDouble` and `LastValueLong`. Also support them in 
+  stats exporters and zpages. Please note that there is an API breaking change
+  in methods `Aggregation.match()` and `AggregationData.match()`.
+
+## 0.12.3 - 2018-04-13
+- Substitute non-ascii characters in B3Format header key.
+
+## 0.12.2 - 2018-02-26
+- Upgrade disruptor to include the fix for SleepingWaitStrategy causing 100%
+  CPU.
+
+## 0.12.1 - 2018-02-26
+- Fix performance issue where unused objects were referenced by the Disruptor.
+- Fix synchonization issue in the use of the Disruptor.
+
+## 0.12.0 - 2018-02-16
+- Rename trace exporters that have inconsistent naming. Exporters with legacy
+  names are deprecated.
+- Fixed bug in CloudTraceFormat that made it impossible to use short span id's.
+- Add `since` Javadoc tag to all APIs.
+- Add a configuration class to create StackdriverTraceExporter.
+- Add MessageEvent and deprecate NetworkEvent.
+- Instana Trace Exporter.
+- Prometheus Stats Exporter.
+- Stats Zpages: RpcZ and StatsZ.
+- Dependency updates.
+
+## 0.11.1 - 2018-01-23
+- Fixed bug that made it impossible to use short span id's (#950).
+
+## 0.11.0 - 2018-01-19
+- Add TextFormat API and two implementations (B3Format and CloudTraceFormat).
+- Add helper class to configure and create StackdriverStatsExporter.
+- Add helper methods in tracer to wrap Runnable and Callbacks and to run them.
+- Increase trace exporting interval to 5s.
+- Add helper class to register views.
+- Make stackdriver stats exporter compatible with GAE Java7.
+- Add SignalFX stats exporter.
+- Add http propagation APIs.
+- Dependency updates.
+
+## 0.10.0 - 2017-12-04
+- Add NoopRunningSpanStore and NoopSampledSpanStore.
+- Change the message event to include (un)compressed sizes for Tracez Zpage.
+- Use AppEngine compatible way to create threads.
+- Add new factory methods that support setting custom Stackdriver
+  MonitoredResource for Stackdriver Stats Exporter.
+- Dependency updates.
+
+## 0.9.1 - 2017-11-29
+- Fix several implementation bugs in Stackdriver Stats Exporter (#830, #831,
+  etc.).
+- Update length limit for View.Name to 255 (previously it's 256).
+
+## 0.9.0 - 2017-11-17
+- Initial stats and tagging implementation for Java (`impl`) and Android
+  (`impl-lite`). This implements all the stats and tagging APIs since v0.8.0.
+- Deprecate Tags.setState and Stats.setState.
+- Add a setStatus method in the Span.
+- OpenCensus Stackdriver Stats Exporter.
+- OpenCensus Stackdriver Trace Exporter is updated to use Stackdriver Trace V2
+  APIs.
+- Dependency updates.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..91279cc
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,158 @@
+# How to submit a bug report
+
+If you received an error message, please include it and any exceptions.
+
+We commonly need to know what platform you are on:
+
+*   JDK/JRE version (i.e., `java -version`)
+*   Operating system (i.e., `uname -a`)
+
+# How to contribute
+
+We definitely welcome patches and contributions to OpenCensus! Here are
+some guidelines and information about how to do so.
+
+## Before getting started
+
+In order to protect both you and ourselves, you will need to sign the
+[Contributor License Agreement](https://cla.developers.google.com/clas).
+
+[Eclipse](https://google-styleguide.googlecode.com/svn/trunk/eclipse-java-google-style.xml)
+and
+[IntelliJ](https://google-styleguide.googlecode.com/svn/trunk/intellij-java-google-style.xml)
+style configurations are commonly useful. For IntelliJ 14, copy the style to
+`~/.IdeaIC14/config/codestyles/`, start IntelliJ, go to File > Settings > Code
+Style, and set the Scheme to `GoogleStyle`.
+
+## Style
+We follow the [Google Java Style
+Guide](https://google.github.io/styleguide/javaguide.html). Our
+build automatically will provide warnings for simple style issues.
+
+Run the following command to format all files. This formatter uses
+[google-java-format](https://github.com/google/google-java-format):
+
+### OS X or Linux
+
+`./gradlew goJF`
+
+### Windows
+
+`gradlew.bat goJF`
+
+We also follow these project-specific guidelines:
+
+### Javadoc
+
+* All public classes and their public and protected methods MUST have javadoc.
+  It MUST be complete (all params documented etc.) Everything else
+  (package-protected classes, private) MAY have javadoc, at the code writer's
+  whim. It does not have to be complete, and reviewers are not allowed to
+  require or disallow it.
+* Each API element should have a `@since` tag specifying the minor version when
+  it was released (or the next minor version).
+* There MUST be NO javadoc errors.
+* See
+  [section 7.3.1](https://google.github.io/styleguide/javaguide.html#s7.3.1-javadoc-exception-self-explanatory)
+  in the guide for exceptions to the Javadoc requirement.
+* Reviewers may request documentation for any element that doesn't require
+  Javadoc, though the style of documentation is up to the author.
+* Try to do the least amount of change when modifying existing documentation.
+  Don't change the style unless you have a good reason.
+
+### AutoValue
+
+* Use [AutoValue](https://github.com/google/auto/tree/master/value), when
+  possible, for any new value classes. Remember to add package-private
+  constructors to all AutoValue classes to prevent classes in other packages
+  from extending them.
+
+## Building opencensus-java
+
+Continuous integration builds the project, runs the tests, and runs multiple
+types of static analysis.
+
+Run the following commands to build, run tests and most static analysis, and
+check formatting:
+
+### OS X or Linux
+
+`./gradlew clean assemble check verGJF`
+
+### Windows
+
+`gradlew.bat clean assemble check verGJF`
+
+Use these commands to run Checker Framework null analysis:
+
+### OS X or Linux
+
+`./gradlew clean assemble -PcheckerFramework`
+
+### Windows
+
+`gradlew.bat clean assemble -PcheckerFramework`
+
+### Checker Framework null analysis
+
+OpenCensus uses the [Checker Framework](https://checkerframework.org/) to
+prevent NullPointerExceptions. Since the project uses Java 6, and Java 6 doesn't
+allow annotations on types, all Checker Framework type annotations must be
+[put in comments](https://checkerframework.org/manual/#backward-compatibility).
+Putting all Checker Framework annotations and imports in comments also avoids a
+dependency on the Checker Framework library.
+
+OpenCensus uses `org.checkerframework.checker.nullness.qual.Nullable` for all
+nullable annotations on types, since `javax.annotation.Nullable` cannot be
+applied to types. However, it uses `javax.annotation.Nullable` in API method
+signatures whenever possible, so that the annotations can be uncommented and
+be included in .class files and Javadocs.
+
+### Checkstyle import control
+
+This project uses Checkstyle to specify the allowed dependencies between
+packages, using its ImportControl feature
+(http://checkstyle.sourceforge.net/config_imports.html#ImportControl).
+`buildscripts/import-control.xml` specifies the allowed imports and contains
+some guidelines on OpenCensus' inter-package dependencies. An error messsage
+such as
+`Disallowed import - edu.umd.cs.findbugs.annotations.SuppressFBWarnings. [ImportControl]`
+could mean that `import-control.xml` needs to be updated.
+
+## Benchmarks
+
+### Invoke all benchmarks on a sub-project
+
+```bash
+$ ./gradlew clean :opencensus-impl-core:jmh
+```
+
+### Invoke on a single benchmark class
+
+```bash
+./gradlew -PjmhIncludeSingleClass=BinaryFormatImplBenchmark clean :opencensus-impl-core:jmh
+```
+
+### Debug compilation errors
+When you make incompatible changes in the Benchmarks classes you may get compilation errors which
+are related to the old code not being compatible with the new code. Some of the reasons are:
+* Any plugin cannot delete the generated code (jmh generates code) because if the user configured
+the directory as the same as source code the plugin will delete users source code.
+* After you run jmh, a gradle daemon will stay alive which may cache the generated code in memory
+and use use that generated code even if the files were changed. This is an issue for classes
+generated with auto-value.
+
+Run this commands to clean the Gradle's cache:
+```bash
+./gradlew --stop
+rm -fr .gradle/
+rm -fr benchmarks/build
+```
+
+## Proposing changes
+
+Create a Pull Request with your changes. Please add any user-visible changes to
+CHANGELOG.md. The continuous integration build will run the tests and static
+analysis. It will also check that the pull request branch has no merge commits.
+When the changes are accepted, they will be merged or cherry-picked by an
+OpenCensus core developer.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0859e7d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,306 @@
+# OpenCensus - A stats collection and distributed tracing framework
+[![Gitter chat][gitter-image]][gitter-url]
+[![Maven Central][maven-image]][maven-url]
+[![Javadocs][javadoc-image]][javadoc-url]
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Coverage Status][codecov-image]][codecov-url]
+
+
+OpenCensus is a toolkit for collecting application performance and behavior data. It currently
+includes 3 apis: stats, tracing and tags.
+
+The library is in [Beta](#versioning) stage and APIs are expected to be mostly stable. The 
+library is expected to move to [GA](#versioning) stage after v1.0.0 major release.
+
+Please join [gitter](https://gitter.im/census-instrumentation/Lobby) for help or feedback on this
+project.
+
+## OpenCensus Quickstart for Libraries
+
+Integrating OpenCensus with a new library means recording stats or traces and propagating context.
+For application integration please see [Quickstart for Applications](https://github.com/census-instrumentation/opencensus-java#quickstart-for-applications).
+
+The full quick start example can also be found on the [OpenCensus website](https://opencensus.io/java/index.html).
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+```
+
+For Bazel add the following lines to the WORKSPACE file:
+```
+maven_jar(
+    name = "io_opencensus_opencensus_api",
+    artifact = "io.opencensus:opencensus-api:0.15.0",
+    sha1 = "9a098392b287d7924660837f4eba0ce252013683",
+)
+```
+Then targets can specify `@io_opencensus_opencensus_api//jar` as a dependency to depend on this jar:
+```bazel
+deps = [
+    "@io_opencensus_opencensus_api//jar",
+]
+```
+You may also need to import the transitive dependencies. See [generate external dependencies from 
+Maven projects](https://docs.bazel.build/versions/master/generate-workspace.html).
+
+### Hello "OpenCensus" trace events
+
+Here's an example of creating a Span and record some trace annotations. Notice that recording the
+annotations is possible because we propagate scope. 3rd parties libraries like SLF4J can integrate
+the same way.
+
+```java
+import io.opencensus.common.Scope;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+
+public final class MyClassWithTracing {
+  private static final Tracer tracer = Tracing.getTracer();
+
+  public static void doWork() {
+    // Create a child Span of the current Span. Always record events for this span and force it to
+    // be sampled. This makes it easier to try out the example, but unless you have a clear use
+    // case, you don't need to explicitly set record events or sampler.
+    try (Scope ss =
+        tracer
+            .spanBuilder("MyChildWorkSpan")
+            .setRecordEvents(true)
+            .setSampler(Samplers.alwaysSample())
+            .startScopedSpan()) {
+      doInitialWork();
+      tracer.getCurrentSpan().addAnnotation("Finished initial work");
+      doFinalWork();
+    }
+  }
+
+  private static void doInitialWork() {
+    // ...
+    tracer.getCurrentSpan().addAnnotation("Important.");
+    // ...
+  }
+
+  private static void doFinalWork() {
+    // ...
+    tracer.getCurrentSpan().addAnnotation("More important.");
+    // ...
+  }
+}
+```
+
+### Hello "OpenCensus" stats events
+
+Here's an example on
+ * defining TagKey, Measure and View,
+ * registering a view,
+ * putting TagKey and TagValue into a scoped TagContext,
+ * recording stats against current TagContext,
+ * getting ViewData.
+
+ 
+For the complete example, see
+[here](https://github.com/census-instrumentation/opencensus-java/blob/master/examples/src/main/java/io/opencensus/examples/helloworld/QuickStart.java).
+
+```java
+import io.opencensus.common.Scope;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.Tags;
+import java.util.Arrays;
+import java.util.Collections;
+
+public final class MyClassWithStats {
+  private static final Tagger tagger = Tags.getTagger();
+  private static final ViewManager viewManager = Stats.getViewManager();
+  private static final StatsRecorder statsRecorder = Stats.getStatsRecorder();
+
+  // frontendKey allows us to break down the recorded data
+  private static final TagKey FRONTEND_KEY = TagKey.create("myorg_keys_frontend");
+
+  // videoSize will measure the size of processed videos.
+  private static final MeasureLong VIDEO_SIZE =
+      MeasureLong.create("my.org/measure/video_size", "size of processed videos", "By");
+
+  // Create view to see the processed video size distribution broken down by frontend.
+  // The view has bucket boundaries (0, 256, 65536) that will group measure values into
+  // histogram buckets.
+  private static final View.Name VIDEO_SIZE_VIEW_NAME = View.Name.create("my.org/views/video_size");
+  private static final View VIDEO_SIZE_VIEW =
+      View.create(
+          VIDEO_SIZE_VIEW_NAME,
+          "processed video size over time",
+          VIDEO_SIZE,
+          Aggregation.Distribution.create(
+              BucketBoundaries.create(Arrays.asList(0.0, 256.0, 65536.0))),
+          Collections.singletonList(FRONTEND_KEY));
+
+  public static void initialize() {
+    // ...
+    viewManager.registerView(VIDEO_SIZE_VIEW);
+  }
+
+  public static void processVideo() {
+    try (Scope scopedTags =
+        tagger
+            .currentBuilder()
+            .put(FRONTEND_KEY, TagValue.create("mobile-ios9.3.5"))
+            .buildScoped()) {
+      // Processing video.
+      // ...
+
+      // Record the processed video size.
+      statsRecorder.newMeasureMap().put(VIDEO_SIZE, 25648).record();
+    }
+  }
+
+  public static void printStats() {
+    ViewData viewData = viewManager.getView(VIDEO_SIZE_VIEW_NAME);
+    System.out.println(
+        String.format("Recorded stats for %s:\n %s", VIDEO_SIZE_VIEW_NAME.asString(), viewData));
+  }
+}
+```
+
+## OpenCensus Quickstart for Applications
+
+Besides recording tracing/stats events the application also need to link the implementation,
+setup exporters, and debugging [Z-Pages](https://github.com/census-instrumentation/opencensus-java/tree/master/contrib/zpages).
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+For Bazel add the following lines to the WORKSPACE file:
+```
+maven_jar(
+    name = "io_opencensus_opencensus_api",
+    artifact = "io.opencensus:opencensus-api:0.15.0",
+    sha1 = "9a098392b287d7924660837f4eba0ce252013683",
+)
+
+maven_jar(
+    name = "io_opencensus_opencensus_impl_core",
+    artifact = "io.opencensus:opencensus-impl-core:0.15.0",
+    sha1 = "36c775926ba1e54af7c37d0503cfb99d986f6229",
+)
+
+maven_jar(
+    name = "io_opencensus_opencensus_impl",
+    artifact = "io.opencensus:opencensus-impl:0.15.0",
+    sha1 = "d7bf0d7ee5a0594f840271c11c9f8d6f754f35d6",
+)
+```
+Then add the following lines to BUILD.bazel file:
+```bazel
+deps = [
+    "@io_opencensus_opencensus_api//jar",
+]
+runtime_deps = [
+    "@io_opencensus_opencensus_impl_core//jar",
+    "@io_opencensus_opencensus_impl//jar",
+]
+```
+Again you may need to import the transitive dependencies. See [generate external dependencies from 
+Maven projects](https://docs.bazel.build/versions/master/generate-workspace.html).
+
+### How to setup exporters?
+
+#### Trace exporters
+* [Instana][TraceExporterInstana]
+* [Jaeger][TraceExporterJaeger]
+* [Logging][TraceExporterLogging]
+* [Stackdriver][TraceExporterStackdriver]
+* [Zipkin][TraceExporterZipkin]
+
+#### Stats exporters
+* [Stackdriver][StatsExporterStackdriver]
+* [SignalFx][StatsExporterSignalFx]
+* [Prometheus][StatsExporterPrometheus]
+
+### How to setup debugging Z-Pages?
+
+If the application owner wants to export in-process tracing and stats data via HTML debugging pages
+see this [link](https://github.com/census-instrumentation/opencensus-java/tree/master/contrib/zpages#quickstart).
+
+## Versioning
+  
+This library follows [Semantic Versioning][semver].
+  
+**GA**: Libraries defined at a GA quality level are stable, and will not introduce 
+backwards-incompatible changes in any minor or patch releases. We will address issues and requests 
+with the highest priority. If we were to make a backwards-incompatible changes on an API, we will 
+first mark the existing API as deprecated and keep it for 18 months before removing it.
+  
+**Beta**: Libraries defined at a Beta quality level are expected to be mostly stable and we're 
+working towards their release candidate. We will address issues and requests with a higher priority.
+There may be backwards incompatible changes in a minor version release, though not in a patch 
+release. If an element is part of an API that is only meant to be used by exporters or other 
+opencensus libraries, then there is no deprecation period. Otherwise, we will deprecate it for 18 
+months before removing it, if possible.
+
+[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
+[javadoc-image]: https://www.javadoc.io/badge/io.opencensus/opencensus-api.svg
+[javadoc-url]: https://www.javadoc.io/doc/io.opencensus/opencensus-api
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-api/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-api
+[gitter-image]: https://badges.gitter.im/census-instrumentation/lobby.svg
+[gitter-url]: https://gitter.im/census-instrumentation/lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
+[codecov-image]: https://codecov.io/gh/census-instrumentation/opencensus-java/branch/master/graph/badge.svg
+[codecov-url]: https://codecov.io/gh/census-instrumentation/opencensus-java/branch/master/
+[semver]: http://semver.org/
+[TraceExporterInstana]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/instana#quickstart
+[TraceExporterJaeger]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/jaeger#quickstart
+[TraceExporterLogging]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/logging#quickstart
+[TraceExporterStackdriver]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/stackdriver#quickstart
+[TraceExporterZipkin]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/zipkin#quickstart
+[StatsExporterStackdriver]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/stats/stackdriver#quickstart
+[StatsExporterSignalFx]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/stats/signalfx#quickstart
+[StatsExporterPrometheus]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/stats/prometheus#quickstart
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..649ac81
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,294 @@
+# How to Create a Release of OpenCensus Java (for Maintainers Only)
+
+## Build Environments
+
+We deploy OpenCensus Java to Maven Central under the following systems:
+
+-   Ubuntu 14.04
+
+Other systems may also work, but we haven't verified them.
+
+## Prerequisites
+
+### Setup OSSRH and Signing
+
+If you haven't deployed artifacts to Maven Central before, you need to setup
+your OSSRH (OSS Repository Hosting) account and signing keys.
+
+-   Follow the instructions on [this
+    page](http://central.sonatype.org/pages/ossrh-guide.html) to set up an
+    account with OSSRH.
+    -   You only need to create the account, not set up a new project
+    -   Contact a OpenCensus Java maintainer to add your account after you
+        have created it.
+-   (For release deployment only) [Install
+    GnuPG](http://central.sonatype.org/pages/working-with-pgp-signatures.html#installing-gnupg)
+    and [generate your key
+    pair](http://central.sonatype.org/pages/working-with-pgp-signatures.html#generating-a-key-pair).
+    You'll also need to [publish your public
+    key](http://central.sonatype.org/pages/working-with-pgp-signatures.html#distributing-your-public-key)
+    to make it visible to the Sonatype servers.
+-   Put your GnuPG key password and OSSRH account information in
+    `<your-home-directory>/.gradle/gradle.properties`:
+
+    ```
+    # You need the signing properties only if you are making release deployment
+    signing.keyId=<8-character-public-key-id>
+    signing.password=<key-password>
+    signing.secretKeyRingFile=<your-home-directory>/.gnupg/secring.gpg
+
+    ossrhUsername=<ossrh-username>
+    ossrhPassword=<ossrh-password>
+    checkstyle.ignoreFailures=false
+    ```
+
+## Tagging the Release
+
+The first step in the release process is to create a release branch, bump
+versions, and create a tag for the release. Our release branches follow the
+naming convention of `v<major>.<minor>.x`, while the tags include the patch
+version `v<major>.<minor>.<patch>`. For example, the same branch `v0.4.x` would
+be used to create all `v0.4` tags (e.g. `v0.4.0`, `v0.4.1`).
+
+In this section upstream repository refers to the main opencensus-java github
+repository.
+
+Before any push to the upstream repository you need to create a [personal access
+token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/).
+
+1.  Create the release branch and push it to GitHub:
+
+    ```bash
+    $ MAJOR=0 MINOR=4 PATCH=0 # Set appropriately for new release
+    $ VERSION_FILES=(
+      build.gradle
+      examples/build.gradle
+      examples/pom.xml
+      api/src/main/java/io/opencensus/common/OpenCensusLibraryInformation.java
+      exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java
+      )
+    $ git checkout -b v$MAJOR.$MINOR.x master
+    $ git push upstream v$MAJOR.$MINOR.x
+    ```
+    The branch will be automatically protected by the GitHub branch protection rule for release
+    branches.
+
+2.  For `master` branch:
+
+    -   Change root build files to the next minor snapshot (e.g.
+        `0.5.0-SNAPSHOT`).
+
+    ```bash
+    $ git checkout -b bump-version master
+    # Change version to next minor (and keep -SNAPSHOT)
+    $ sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*CURRENT_OPENCENSUS_VERSION\)/'$MAJOR.$((MINOR+1)).0'\1/' \
+      "${VERSION_FILES[@]}"
+    $ ./gradlew build
+    $ git commit -a -m "Start $MAJOR.$((MINOR+1)).0 development cycle"
+    ```
+
+    -   Go through PR review and push the master branch to GitHub:
+
+    ```bash
+    $ git checkout master
+    $ git merge --ff-only bump-version
+    $ git push upstream master
+    ```
+
+3.  For `vMajor.Minor.x` branch:
+
+    -   Change root build files to remove "-SNAPSHOT" for the next release
+        version (e.g. `0.4.0`). Commit the result and make a tag:
+
+    ```bash
+    $ git checkout -b release v$MAJOR.$MINOR.x
+    # Change version to remove -SNAPSHOT
+    $ sed -i 's/-SNAPSHOT\(.*CURRENT_OPENCENSUS_VERSION\)/\1/' "${VERSION_FILES[@]}"
+    $ ./gradlew build
+    $ git commit -a -m "Bump version to $MAJOR.$MINOR.$PATCH"
+    $ git tag -a v$MAJOR.$MINOR.$PATCH -m "Version $MAJOR.$MINOR.$PATCH"
+    ```
+
+    -   Change root build files to the next snapshot version (e.g.
+        `0.4.1-SNAPSHOT`). Commit the result:
+
+    ```bash
+    # Change version to next patch and add -SNAPSHOT
+    $ sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*CURRENT_OPENCENSUS_VERSION\)/'$MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT'\1/' \
+     "${VERSION_FILES[@]}"
+    $ ./gradlew build
+    $ git commit -a -m "Bump version to $MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT"
+    ```
+
+    -   Go through PR review and push the release tag and updated release branch
+        to GitHub:
+
+    ```bash
+    $ git checkout v$MAJOR.$MINOR.x
+    $ git merge --ff-only release
+    $ git push upstream v$MAJOR.$MINOR.$PATCH
+    $ git push upstream v$MAJOR.$MINOR.x
+    ```
+
+## Deployment
+
+Deployment to Maven Central (or the snapshot repo) is for all of the artifacts
+from the project.
+
+### Branch
+
+Before building/deploying, be sure to switch to the appropriate tag. The tag
+must reference a commit that has been pushed to the main repository, i.e., has
+gone through code review. For the current release use:
+
+```bash
+$ git checkout -b v$MAJOR.$MINOR.$PATCH tags/v$MAJOR.$MINOR.$PATCH
+```
+
+### Initial Deployment
+
+The following command will build the whole project and upload it to Maven
+Central. Parallel building [is not safe during
+uploadArchives](https://issues.gradle.org/browse/GRADLE-3420).
+
+```bash
+$ ./gradlew clean build && ./gradlew -Dorg.gradle.parallel=false uploadArchives
+```
+
+If the version has the `-SNAPSHOT` suffix, the artifacts will automatically go
+to the snapshot repository. Otherwise it's a release deployment and the
+artifacts will go to a staging repository.
+
+When deploying a Release, the deployment will create [a new staging
+repository](https://oss.sonatype.org/#stagingRepositories). You'll need to look
+up the ID in the OSSRH UI (usually in the form of `opencensus-*`).
+
+## Releasing on Maven Central
+
+Once all of the artifacts have been pushed to the staging repository, the
+repository must first be `closed`, which will trigger several sanity checks on
+the repository. If this completes successfully, the repository can then be
+`released`, which will begin the process of pushing the new artifacts to Maven
+Central (the staging repository will be destroyed in the process). You can see
+the complete process for releasing to Maven Central on the [OSSRH
+site](http://central.sonatype.org/pages/releasing-the-deployment.html).
+
+## Announcement
+
+Once deployment is done, go to Github [release
+page](https://github.com/census-instrumentation/opencensus-java/releases), press
+`Draft a new release` to write release notes about the new release.
+
+You can use `git log upstream/v$MAJOR.$((MINOR-1)).x..upstream/v$MAJOR.$MINOR.x --graph --first-parent`
+or the Github [compare tool](https://github.com/census-instrumentation/opencensus-java/compare/)
+to view a summary of all commits since last release as a reference. In addition, you can refer to 
+[CHANGELOG.md](https://github.com/census-instrumentation/opencensus-java/blob/master/CHANGELOG.md)
+for a list of major changes since last release.
+
+Please pick major or important user-visible changes only.
+
+## Update release versions in documentations and build files
+
+After releasing is done, you need to update all readmes and examples to point to the
+latest version.
+
+1. Update README.md and gradle/maven build files on `master` branch:
+
+```bash
+$ git checkout -b bump-document-version master
+$ BUILD_FILES=(
+  examples/build.gradle
+  examples/pom.xml
+  )
+$ README_FILES=(
+  README.md
+  contrib/appengine_standard_util/README.md
+  contrib/exemplar_util/README.md
+  contrib/grpc_util/README.md
+  contrib/http_util/README.md
+  contrib/log_correlation/log4j2/README.md
+  contrib/log_correlation/stackdriver/README.md
+  contrib/monitored_resource_util/README.md
+  contrib/spring/README.md
+  contrib/spring_sleuth_v1x/README.md
+  contrib/zpages/README.md
+  exporters/stats/prometheus/README.md
+  exporters/stats/signalfx/README.md
+  exporters/stats/stackdriver/README.md
+  exporters/trace/instana/README.md
+  exporters/trace/logging/README.md
+  exporters/trace/jaeger/README.md
+  exporters/trace/ocagent/README.md
+  exporters/trace/stackdriver/README.md
+  exporters/trace/zipkin/README.md
+  )
+# Substitute versions in build files
+$ sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*LATEST_OPENCENSUS_RELEASE_VERSION\)/'$MAJOR.$MINOR.$PATCH'\1/' \
+ "${BUILD_FILES[@]}"
+# Substitute versions in build.gradle examples in README.md
+$ sed -i 's/\(\(compile\|runtime\).\+io\.opencensus:.\+:\)[0-9]\+\.[0-9]\+\.[0-9]\+/\1'$MAJOR.$MINOR.$PATCH'/' \
+ "${README_FILES[@]}"
+# Substitute versions in maven pom examples in README.md
+$ sed -i 's/\(<version>\)[0-9]\+\.[0-9]\+\.[0-9]\+/\1'$MAJOR.$MINOR.$PATCH'/' \
+ "${README_FILES[@]}"
+```
+
+2. Update bazel dependencies for subproject `examples`:
+
+    - Follow the instructions on [this
+    page](https://docs.bazel.build/versions/master/generate-workspace.html) to
+    install bazel migration tool. You may also need to manually apply
+    this [patch](
+    https://github.com/nevillelyh/migration-tooling/commit/f10e14fd18ad3885c7ec8aa305e4eba266a07ebf)
+    if you encounter `Unable to find a version for ... due to Invalid Range Result` error when
+    using it.
+
+    - Use the following command to generate new dependencies file:
+
+    ```bash
+    $ bazel run //generate_workspace -- \
+    --artifact=com.google.guava:guava-jdk5:23.0
+    --artifact=com.google.guava:guava:23.0 \
+    --artifact=io.grpc:grpc-all:1.9.0 \
+    --artifact=io.opencensus:opencensus-api:$MAJOR.$MINOR.$PATCH \
+    --artifact=io.opencensus:opencensus-contrib-grpc-metrics:$MAJOR.$MINOR.$PATCH \
+    --artifact=io.opencensus:opencensus-contrib-zpages:$MAJOR.$MINOR.$PATCH \
+    --artifact=io.opencensus:opencensus-exporter-stats-prometheus:$MAJOR.$MINOR.$PATCH \
+    --artifact=io.opencensus:opencensus-exporter-stats-stackdriver:$MAJOR.$MINOR.$PATCH \
+    --artifact=io.opencensus:opencensus-exporter-trace-logging:$MAJOR.$MINOR.$PATCH \
+    --artifact=io.opencensus:opencensus-exporter-trace-stackdriver:$MAJOR.$MINOR.$PATCH \
+    --artifact=io.opencensus:opencensus-impl:$MAJOR.$MINOR.$PATCH \
+    --artifact=io.prometheus:simpleclient_httpserver:0.3.0 \
+    --repositories=http://repo.maven.apache.org/maven2
+    Wrote
+    /usr/local/.../generate_workspace.runfiles/__main__/generate_workspace.bzl
+    ```
+
+    - Copy this file to overwrite `examples/opencensus_workspace.bzl`.
+
+    - Use the following command to rename the generated rules and commit the
+      changes above:
+
+    ```bash
+    $ sed -i 's/def generated_/def opencensus_/' examples/opencensus_workspace.bzl
+    $ git commit -a -m "Update release versions for all readme and build files."
+    ```
+
+3. Go through PR review and merge it to GitHub master branch.
+
+4. In addition, create a PR to mark the new release in 
+[CHANGELOG.md](https://github.com/census-instrumentation/opencensus-java/blob/master/CHANGELOG.md)
+on master branch. Once that PR is merged, cherry-pick the commit and create another PR to the 
+release branch (branch v$MAJOR.$MINOR.x).
+
+
+## Known Issues
+
+### Deployment for tag v0.5.0
+To rebuild the releases on the tag v0.5.0 use:
+```bash
+$ ./gradlew clean build && ./gradlew uploadArchives
+```
+
+If option `-Dorg.gradle.parallel=false` is used, you will hit [this bug](https://issues.sonatype.org/browse/OSSRH-19485)
+caused by [this bug](https://github.com/gradle/gradle/issues/1827) in gradle 3.5.
diff --git a/all/build.gradle b/all/build.gradle
new file mode 100644
index 0000000..83ffb69
--- /dev/null
+++ b/all/build.gradle
@@ -0,0 +1,105 @@
+description = "OpenCensus All"
+
+def subprojects = [
+        project(':opencensus-api'),
+        project(':opencensus-impl-core'),
+        project(':opencensus-impl'),
+        project(':opencensus-impl-lite'),
+        project(':opencensus-testing'),
+        project(':opencensus-contrib-agent'),
+        project(':opencensus-contrib-appengine-standard-util'),
+        project(':opencensus-contrib-dropwizard'),
+        project(':opencensus-contrib-exemplar-util'),
+        project(':opencensus-contrib-grpc-util'),
+        project(':opencensus-contrib-grpc-metrics'),
+        project(':opencensus-contrib-http-util'),
+        project(':opencensus-contrib-log-correlation-log4j2'),
+        project(':opencensus-contrib-log-correlation-stackdriver'),
+        project(':opencensus-contrib-monitored-resource-util'),
+        project(':opencensus-contrib-spring'),
+        project(':opencensus-contrib-spring-sleuth-v1x'),
+        project(':opencensus-contrib-zpages'),
+        project(':opencensus-exporter-trace-logging'),
+        project(':opencensus-exporter-trace-ocagent'),
+        project(':opencensus-exporter-trace-stackdriver'),
+        project(':opencensus-exporter-trace-zipkin'),
+        project(':opencensus-exporter-trace-jaeger'),
+        project(':opencensus-exporter-stats-signalfx'),
+        project(':opencensus-exporter-stats-stackdriver'),
+        project(':opencensus-exporter-stats-prometheus'),
+]
+
+// A subset of subprojects for which we want to publish javadoc.
+def subprojects_javadoc = [
+        project(':opencensus-api'),
+        project(':opencensus-testing'),
+        project(':opencensus-contrib-agent'),
+        project(':opencensus-contrib-appengine-standard-util'),
+        project(':opencensus-contrib-dropwizard'),
+        project(':opencensus-contrib-exemplar-util'),
+        project(':opencensus-contrib-grpc-util'),
+        project(':opencensus-contrib-grpc-metrics'),
+        project(':opencensus-contrib-http-util'),
+        project(':opencensus-contrib-log-correlation-log4j2'),
+        project(':opencensus-contrib-log-correlation-stackdriver'),
+        project(':opencensus-contrib-monitored-resource-util'),
+        project(':opencensus-contrib-spring'),
+        project(':opencensus-contrib-spring-sleuth-v1x'),
+        project(':opencensus-contrib-zpages'),
+        project(':opencensus-exporter-trace-logging'),
+        project(':opencensus-exporter-trace-ocagent'),
+        project(':opencensus-exporter-trace-stackdriver'),
+        project(':opencensus-exporter-trace-zipkin'),
+        project(':opencensus-exporter-trace-jaeger'),
+        project(':opencensus-exporter-stats-signalfx'),
+        project(':opencensus-exporter-stats-stackdriver'),
+        project(':opencensus-exporter-stats-prometheus'),
+]
+
+for (subproject in rootProject.subprojects) {
+    if (subproject == project) {
+        continue
+    }
+    evaluationDependsOn(subproject.path)
+}
+
+dependencies {
+    compile subprojects
+}
+
+javadoc {
+    classpath = files(subprojects_javadoc.collect { subproject ->
+        subproject.javadoc.classpath
+    })
+    for (subproject in subprojects_javadoc) {
+        if (subproject == project) {
+            continue;
+        }
+        source subproject.javadoc.source
+        options.links subproject.javadoc.options.links.toArray(new String[0])
+    }
+    exclude 'io/opencensus/internal/**'
+}
+
+task jacocoMerge(type: JacocoMerge) {
+    dependsOn(subprojects.jacocoTestReport.dependsOn)
+    mustRunAfter(subprojects.jacocoTestReport.mustRunAfter)
+    destinationFile = file("${buildDir}/jacoco/test.exec")
+    executionData = files(subprojects.jacocoTestReport.executionData)
+            .filter { f -> f.exists() }
+}
+
+jacocoTestReport {
+    dependsOn(jacocoMerge)
+    reports {
+        xml.enabled = true
+        html.enabled = true
+    }
+
+    additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
+    sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs)
+    classDirectories = files(subprojects.sourceSets.main.output)
+    classDirectories = files(classDirectories.files.collect {
+        fileTree(dir: it)
+    })
+}
diff --git a/api/README.md b/api/README.md
new file mode 100644
index 0000000..83891d4
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,6 @@
+OpenCensus API
+======================================================
+
+* Java 6 and Android compatible.
+* The abstract classes in this directory can be subclassed to create alternative
+  implementations of the OpenCensus library.
diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 0000000..31274ca
--- /dev/null
+++ b/api/build.gradle
@@ -0,0 +1,15 @@
+description = 'OpenCensus API'
+
+dependencies {
+    compile libraries.grpc_context
+
+    compileOnly libraries.auto_value
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
+
+javadoc {
+    exclude 'io/opencensus/internal/**'
+    exclude 'io/opencensus/trace/internal/**'
+}
diff --git a/api/src/main/java/io/opencensus/common/Clock.java b/api/src/main/java/io/opencensus/common/Clock.java
new file mode 100644
index 0000000..cd31193
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Clock.java
@@ -0,0 +1,43 @@
+/*
+ * 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.common;
+
+/**
+ * Interface for getting the current time.
+ *
+ * @since 0.5
+ */
+public abstract class Clock {
+
+  /**
+   * Obtains the current instant from this clock.
+   *
+   * @return the current instant.
+   * @since 0.5
+   */
+  public abstract Timestamp now();
+
+  /**
+   * Returns a time measurement with nanosecond precision that can only be used to calculate elapsed
+   * time.
+   *
+   * @return a time measurement with nanosecond precision that can only be used to calculate elapsed
+   *     time.
+   * @since 0.5
+   */
+  public abstract long nowNanos();
+}
diff --git a/api/src/main/java/io/opencensus/common/Duration.java b/api/src/main/java/io/opencensus/common/Duration.java
new file mode 100644
index 0000000..f46cd18
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Duration.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2016-17, 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.common;
+
+import static io.opencensus.common.TimeUtils.MAX_NANOS;
+import static io.opencensus.common.TimeUtils.MAX_SECONDS;
+import static io.opencensus.common.TimeUtils.MILLIS_PER_SECOND;
+import static io.opencensus.common.TimeUtils.NANOS_PER_MILLI;
+
+import com.google.auto.value.AutoValue;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Represents a signed, fixed-length span of time represented as a count of seconds and fractions of
+ * seconds at nanosecond resolution. It is independent of any calendar and concepts like "day" or
+ * "month". Range is approximately +-10,000 years.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class Duration implements Comparable<Duration> {
+
+  /**
+   * Creates a new time duration from given seconds and nanoseconds.
+   *
+   * @param seconds Signed seconds of the span of time. Must be from -315,576,000,000 to
+   *     +315,576,000,000 inclusive.
+   * @param nanos Signed fractions of a second at nanosecond resolution of the span of time.
+   *     Durations less than one second are represented with a 0 `seconds` field and a positive or
+   *     negative `nanos` field. For durations of one second or more, a non-zero value for the
+   *     `nanos` field must be of the same sign as the `seconds` field. Must be from -999,999,999 to
+   *     +999,999,999 inclusive.
+   * @return new {@code Duration} with specified fields.
+   * @throws IllegalArgumentException if the arguments are out of range or have inconsistent sign.
+   * @since 0.5
+   */
+  public static Duration create(long seconds, int nanos) {
+    if (seconds < -MAX_SECONDS) {
+      throw new IllegalArgumentException(
+          "'seconds' is less than minimum (" + -MAX_SECONDS + "): " + seconds);
+    }
+    if (seconds > MAX_SECONDS) {
+      throw new IllegalArgumentException(
+          "'seconds' is greater than maximum (" + MAX_SECONDS + "): " + seconds);
+    }
+    if (nanos < -MAX_NANOS) {
+      throw new IllegalArgumentException(
+          "'nanos' is less than minimum (" + -MAX_NANOS + "): " + nanos);
+    }
+    if (nanos > MAX_NANOS) {
+      throw new IllegalArgumentException(
+          "'nanos' is greater than maximum (" + MAX_NANOS + "): " + nanos);
+    }
+    if ((seconds < 0 && nanos > 0) || (seconds > 0 && nanos < 0)) {
+      throw new IllegalArgumentException(
+          "'seconds' and 'nanos' have inconsistent sign: seconds=" + seconds + ", nanos=" + nanos);
+    }
+    return new AutoValue_Duration(seconds, nanos);
+  }
+
+  /**
+   * Creates a new {@code Duration} from given milliseconds.
+   *
+   * @param millis the duration in milliseconds.
+   * @return a new {@code Duration} from given milliseconds.
+   * @throws IllegalArgumentException if the number of milliseconds is out of the range that can be
+   *     represented by {@code Duration}.
+   * @since 0.5
+   */
+  public static Duration fromMillis(long millis) {
+    long seconds = millis / MILLIS_PER_SECOND;
+    int nanos = (int) (millis % MILLIS_PER_SECOND * NANOS_PER_MILLI);
+    return Duration.create(seconds, nanos);
+  }
+
+  /**
+   * Converts a {@link Duration} to milliseconds.
+   *
+   * @return the milliseconds representation of this {@code Duration}.
+   * @since 0.13
+   */
+  public long toMillis() {
+    return TimeUnit.SECONDS.toMillis(getSeconds()) + TimeUnit.NANOSECONDS.toMillis(getNanos());
+  }
+
+  /**
+   * Returns the number of seconds in the {@code Duration}.
+   *
+   * @return the number of seconds in the {@code Duration}.
+   * @since 0.5
+   */
+  public abstract long getSeconds();
+
+  /**
+   * Returns the number of nanoseconds in the {@code Duration}.
+   *
+   * @return the number of nanoseconds in the {@code Duration}.
+   * @since 0.5
+   */
+  public abstract int getNanos();
+
+  /**
+   * Compares this {@code Duration} to the specified {@code Duration}.
+   *
+   * @param otherDuration the other {@code Duration} to compare to, not {@code null}.
+   * @return the comparator value: zero if equal, negative if this duration is smaller than
+   *     otherDuration, positive if larger.
+   * @throws NullPointerException if otherDuration is {@code null}.
+   */
+  @Override
+  public int compareTo(Duration otherDuration) {
+    int cmp = TimeUtils.compareLongs(getSeconds(), otherDuration.getSeconds());
+    if (cmp != 0) {
+      return cmp;
+    }
+    return TimeUtils.compareLongs(getNanos(), otherDuration.getNanos());
+  }
+
+  Duration() {}
+}
diff --git a/api/src/main/java/io/opencensus/common/ExperimentalApi.java b/api/src/main/java/io/opencensus/common/ExperimentalApi.java
new file mode 100644
index 0000000..7a4da7c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ExperimentalApi.java
@@ -0,0 +1,58 @@
+/*
+ * 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.common;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates a public API that can change at any time, and has no guarantee of API stability and
+ * backward-compatibility.
+ *
+ * <p>Usage guidelines:
+ *
+ * <ol>
+ *   <li>This annotation is used only on public API. Internal interfaces should not use it.
+ *   <li>After OpenCensus has gained API stability, this annotation can only be added to new API.
+ *       Adding it to an existing API is considered API-breaking.
+ *   <li>Removing this annotation from an API gives it stable status.
+ * </ol>
+ *
+ * @since 0.8
+ */
+@Internal
+@Retention(RetentionPolicy.SOURCE)
+@Target({
+  ElementType.ANNOTATION_TYPE,
+  ElementType.CONSTRUCTOR,
+  ElementType.FIELD,
+  ElementType.METHOD,
+  ElementType.PACKAGE,
+  ElementType.TYPE
+})
+@Documented
+public @interface ExperimentalApi {
+  /**
+   * Context information such as links to discussion thread, tracking issue etc.
+   *
+   * @since 0.8
+   */
+  String value() default "";
+}
diff --git a/api/src/main/java/io/opencensus/common/Function.java b/api/src/main/java/io/opencensus/common/Function.java
new file mode 100644
index 0000000..a9ed5a9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Function.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016-17, 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.common;
+
+/**
+ * Used to specify matching functions for use encoding tagged unions (i.e. sum types) in Java. See
+ * {@link io.opencensus.trace.AttributeValue#match} for an example of its use.
+ *
+ * <p>Note: This class is based on the java.util.Function class added in Java 1.8. We cannot use the
+ * Function from Java 1.8 because this library is Java 1.6 compatible.
+ *
+ * @since 0.5
+ */
+public interface Function<A, B> {
+
+  /**
+   * Applies the function to the given argument.
+   *
+   * @param arg the argument to the function.
+   * @return the result of the function.
+   * @since 0.5
+   */
+  B apply(A arg);
+}
diff --git a/api/src/main/java/io/opencensus/common/Functions.java b/api/src/main/java/io/opencensus/common/Functions.java
new file mode 100644
index 0000000..ea3457c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Functions.java
@@ -0,0 +1,130 @@
+/*
+ * 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.common;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Commonly used {@link Function} instances.
+ *
+ * @since 0.5
+ */
+public final class Functions {
+  private Functions() {}
+
+  private static final Function<Object, /*@Nullable*/ Void> RETURN_NULL =
+      new Function<Object, /*@Nullable*/ Void>() {
+        @Override
+        @javax.annotation.Nullable
+        public Void apply(Object ignored) {
+          return null;
+        }
+      };
+
+  private static final Function<Object, Void> THROW_ILLEGAL_ARGUMENT_EXCEPTION =
+      new Function<Object, Void>() {
+        @Override
+        public Void apply(Object ignored) {
+          throw new IllegalArgumentException();
+        }
+      };
+
+  private static final Function<Object, Void> THROW_ASSERTION_ERROR =
+      new Function<Object, Void>() {
+        @Override
+        public Void apply(Object ignored) {
+          throw new AssertionError();
+        }
+      };
+
+  private static final Function<Object, /*@Nullable*/ String> RETURN_TO_STRING =
+      new Function<Object, /*@Nullable*/ String>() {
+        @Override
+        public /*@Nullable*/ String apply(Object input) {
+          return input == null ? null : input.toString();
+        }
+      };
+
+  /**
+   * A {@code Function} that always ignores its argument and returns {@code null}.
+   *
+   * @return a {@code Function} that always ignores its argument and returns {@code null}.
+   * @since 0.5
+   */
+  public static <T> Function<Object, /*@Nullable*/ T> returnNull() {
+    // It is safe to cast a producer of Void to anything, because Void is always null.
+    @SuppressWarnings("unchecked")
+    Function<Object, /*@Nullable*/ T> function = (Function<Object, /*@Nullable*/ T>) RETURN_NULL;
+    return function;
+  }
+
+  /**
+   * A {@code Function} that always ignores its argument and returns a constant value.
+   *
+   * @return a {@code Function} that always ignores its argument and returns a constant value.
+   * @since 0.5
+   */
+  public static <T> Function<Object, T> returnConstant(final T constant) {
+    return new Function<Object, T>() {
+      @Override
+      public T apply(Object ignored) {
+        return constant;
+      }
+    };
+  }
+
+  /**
+   * A {@code Function} that always returns the {@link #toString()} value of the input.
+   *
+   * @return a {@code Function} that always returns the {@link #toString()} value of the input.
+   * @since 0.17
+   */
+  public static Function<Object, /*@Nullable*/ String> returnToString() {
+    return RETURN_TO_STRING;
+  }
+
+  /**
+   * A {@code Function} that always ignores its argument and throws an {@link
+   * IllegalArgumentException}.
+   *
+   * @return a {@code Function} that always ignores its argument and throws an {@link
+   *     IllegalArgumentException}.
+   * @since 0.5
+   */
+  public static <T> Function<Object, T> throwIllegalArgumentException() {
+    // It is safe to cast this function to have any return type, since it never returns a result.
+    @SuppressWarnings("unchecked")
+    Function<Object, T> function = (Function<Object, T>) THROW_ILLEGAL_ARGUMENT_EXCEPTION;
+    return function;
+  }
+
+  /**
+   * A {@code Function} that always ignores its argument and throws an {@link AssertionError}.
+   *
+   * @return a {@code Function} that always ignores its argument and throws an {@code
+   *     AssertionError}.
+   * @since 0.6
+   */
+  public static <T> Function<Object, T> throwAssertionError() {
+    // It is safe to cast this function to have any return type, since it never returns a result.
+    @SuppressWarnings("unchecked")
+    Function<Object, T> function = (Function<Object, T>) THROW_ASSERTION_ERROR;
+    return function;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/common/Internal.java b/api/src/main/java/io/opencensus/common/Internal.java
new file mode 100644
index 0000000..d84fba2
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Internal.java
@@ -0,0 +1,42 @@
+/*
+ * 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.common;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotates a program element (class, method, package etc) which is internal to OpenCensus, not
+ * part of the public API, and should not be used by users of the OpenCensus library.
+ *
+ * @since 0.5
+ */
+@Internal
+@Retention(RetentionPolicy.SOURCE)
+@Target({
+  ElementType.ANNOTATION_TYPE,
+  ElementType.CONSTRUCTOR,
+  ElementType.FIELD,
+  ElementType.METHOD,
+  ElementType.PACKAGE,
+  ElementType.TYPE
+})
+@Documented
+public @interface Internal {}
diff --git a/api/src/main/java/io/opencensus/common/NonThrowingCloseable.java b/api/src/main/java/io/opencensus/common/NonThrowingCloseable.java
new file mode 100644
index 0000000..30d07ac
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/NonThrowingCloseable.java
@@ -0,0 +1,42 @@
+/*
+ * 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.common;
+
+import java.io.Closeable;
+
+/**
+ * An {@link Closeable} which cannot throw a checked exception.
+ *
+ * <p>This is useful because such a reversion otherwise requires the caller to catch the
+ * (impossible) Exception in the try-with-resources.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>
+ *   try (NonThrowingAutoCloseable ctx = tryEnter()) {
+ *     ...
+ *   }
+ * </pre>
+ *
+ * @deprecated {@link Scope} is a better match for operations involving the current context.
+ * @since 0.5
+ */
+@Deprecated
+public interface NonThrowingCloseable extends Closeable {
+  @Override
+  void close();
+}
diff --git a/api/src/main/java/io/opencensus/common/OpenCensusLibraryInformation.java b/api/src/main/java/io/opencensus/common/OpenCensusLibraryInformation.java
new file mode 100644
index 0000000..3f659c1
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/OpenCensusLibraryInformation.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016-17, 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.common;
+
+/**
+ * Class holder for all common constants (such as the version) for the OpenCensus Java library.
+ *
+ * @since 0.8
+ */
+@ExperimentalApi
+public final class OpenCensusLibraryInformation {
+
+  /**
+   * The current version of the OpenCensus Java library.
+   *
+   * @since 0.8
+   */
+  public static final String VERSION = "0.17.0-SNAPSHOT"; // CURRENT_OPENCENSUS_VERSION
+
+  private OpenCensusLibraryInformation() {}
+}
diff --git a/api/src/main/java/io/opencensus/common/Scope.java b/api/src/main/java/io/opencensus/common/Scope.java
new file mode 100644
index 0000000..de954f5
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Scope.java
@@ -0,0 +1,37 @@
+/*
+ * 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.common;
+
+/**
+ * A {@link java.io.Closeable} that represents a change to the current context over a scope of code.
+ * {@link Scope#close} cannot throw a checked exception.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>
+ *   try (Scope ctx = tryEnter()) {
+ *     ...
+ *   }
+ * </pre>
+ *
+ * @since 0.6
+ */
+@SuppressWarnings("deprecation")
+public interface Scope extends NonThrowingCloseable {
+  @Override
+  void close();
+}
diff --git a/api/src/main/java/io/opencensus/common/ServerStats.java b/api/src/main/java/io/opencensus/common/ServerStats.java
new file mode 100644
index 0000000..42efa1f
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ServerStats.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018, 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.common;
+
+import com.google.auto.value.AutoValue;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A representation of stats measured on the server side.
+ *
+ * @since 0.16
+ */
+@Immutable
+@AutoValue
+public abstract class ServerStats {
+
+  ServerStats() {}
+
+  /**
+   * Returns Load Balancer latency, a latency observed at Load Balancer.
+   *
+   * @return Load Balancer latency in nanoseconds.
+   * @since 0.16
+   */
+  public abstract long getLbLatencyNs();
+
+  /**
+   * Returns Service latency, a latency observed at Server.
+   *
+   * @return Service latency in nanoseconds.
+   * @since 0.16
+   */
+  public abstract long getServiceLatencyNs();
+
+  /**
+   * Returns Trace options, a set of bits indicating properties of trace.
+   *
+   * @return Trace options a set of bits indicating properties of trace.
+   * @since 0.16
+   */
+  public abstract byte getTraceOption();
+
+  /**
+   * Creates new {@link ServerStats} from specified parameters.
+   *
+   * @param lbLatencyNs Represents request processing latency observed on Load Balancer. It is
+   *     measured in nanoseconds. Must not be less than 0. Value of 0 represents that the latency is
+   *     not measured.
+   * @param serviceLatencyNs Represents request processing latency observed on Server. It is
+   *     measured in nanoseconds. Must not be less than 0. Value of 0 represents that the latency is
+   *     not measured.
+   * @param traceOption Represents set of bits to indicate properties of trace. Currently it used
+   *     only the least signification bit to represent sampling of the request on the server side.
+   *     Other bits are ignored.
+   * @return new {@code ServerStats} with specified fields.
+   * @throws IllegalArgumentException if the arguments are out of range.
+   * @since 0.16
+   */
+  public static ServerStats create(long lbLatencyNs, long serviceLatencyNs, byte traceOption) {
+
+    if (lbLatencyNs < 0) {
+      throw new IllegalArgumentException("'getLbLatencyNs' is less than zero: " + lbLatencyNs);
+    }
+
+    if (serviceLatencyNs < 0) {
+      throw new IllegalArgumentException(
+          "'getServiceLatencyNs' is less than zero: " + serviceLatencyNs);
+    }
+
+    return new AutoValue_ServerStats(lbLatencyNs, serviceLatencyNs, traceOption);
+  }
+}
diff --git a/api/src/main/java/io/opencensus/common/ServerStatsDeserializationException.java b/api/src/main/java/io/opencensus/common/ServerStatsDeserializationException.java
new file mode 100644
index 0000000..2332733
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ServerStatsDeserializationException.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018, 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.common;
+
+/**
+ * Exception thrown when a {@link ServerStats} cannot be parsed.
+ *
+ * @since 0.16
+ */
+public final class ServerStatsDeserializationException extends Exception {
+  private static final long serialVersionUID = 0L;
+
+  /**
+   * Constructs a new {@code ServerStatsDeserializationException} with the given message.
+   *
+   * @param message a message describing the error.
+   * @since 0.16
+   */
+  public ServerStatsDeserializationException(String message) {
+    super(message);
+  }
+
+  /**
+   * Constructs a new {@code ServerStatsDeserializationException} with the given message and cause.
+   *
+   * @param message a message describing the error.
+   * @param cause the cause of the error.
+   * @since 0.16
+   */
+  public ServerStatsDeserializationException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/api/src/main/java/io/opencensus/common/ServerStatsEncoding.java b/api/src/main/java/io/opencensus/common/ServerStatsEncoding.java
new file mode 100644
index 0000000..024a93f
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ServerStatsEncoding.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2018, 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.common;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A service class to encode/decode {@link ServerStats} as defined by the spec.
+ *
+ * <p>See <a
+ * href="https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/CensusServerStatsEncoding.md">opencensus-server-stats-specs</a>
+ * for encoding {@code ServerStats}
+ *
+ * <p>Use {@code ServerStatsEncoding.toBytes(ServerStats stats)} to encode.
+ *
+ * <p>Use {@code ServerStatsEncoding.parseBytes(byte[] serialized)} to decode.
+ *
+ * @since 0.16
+ */
+public final class ServerStatsEncoding {
+
+  private ServerStatsEncoding() {}
+
+  /**
+   * The current encoding version. The value is {@value #CURRENT_VERSION}
+   *
+   * @since 0.16
+   */
+  public static final byte CURRENT_VERSION = (byte) 0;
+
+  /**
+   * Encodes the {@link ServerStats} as per the Opencensus Summary Span specification.
+   *
+   * @param stats {@code ServerStats} to encode.
+   * @return encoded byte array.
+   * @since 0.16
+   */
+  public static byte[] toBytes(ServerStats stats) {
+    // Should this be optimized to not include invalid values?
+
+    ByteBuffer bb = ByteBuffer.allocate(ServerStatsFieldEnums.getTotalSize() + 1);
+    bb.order(ByteOrder.LITTLE_ENDIAN);
+
+    // put version
+    bb.put(CURRENT_VERSION);
+
+    bb.put((byte) ServerStatsFieldEnums.Id.SERVER_STATS_LB_LATENCY_ID.value());
+    bb.putLong(stats.getLbLatencyNs());
+
+    bb.put((byte) ServerStatsFieldEnums.Id.SERVER_STATS_SERVICE_LATENCY_ID.value());
+    bb.putLong(stats.getServiceLatencyNs());
+
+    bb.put((byte) ServerStatsFieldEnums.Id.SERVER_STATS_TRACE_OPTION_ID.value());
+    bb.put(stats.getTraceOption());
+    return bb.array();
+  }
+
+  /**
+   * Decodes serialized byte array to create {@link ServerStats} as per Opencensus Summary Span
+   * specification.
+   *
+   * @param serialized encoded {@code ServerStats} in byte array.
+   * @return decoded {@code ServerStats}. null if decoding fails.
+   * @since 0.16
+   */
+  public static ServerStats parseBytes(byte[] serialized)
+      throws ServerStatsDeserializationException {
+    final ByteBuffer bb = ByteBuffer.wrap(serialized);
+    bb.order(ByteOrder.LITTLE_ENDIAN);
+    long serviceLatencyNs = 0L;
+    long lbLatencyNs = 0L;
+    byte traceOption = (byte) 0;
+
+    // Check the version first.
+    if (!bb.hasRemaining()) {
+      throw new ServerStatsDeserializationException("Serialized ServerStats buffer is empty");
+    }
+    byte version = bb.get();
+
+    if (version > CURRENT_VERSION || version < 0) {
+      throw new ServerStatsDeserializationException("Invalid ServerStats version: " + version);
+    }
+
+    while (bb.hasRemaining()) {
+      ServerStatsFieldEnums.Id id = ServerStatsFieldEnums.Id.valueOf((int) bb.get() & 0xFF);
+      if (id == null) {
+        // Skip remaining;
+        bb.position(bb.limit());
+      } else {
+        switch (id) {
+          case SERVER_STATS_LB_LATENCY_ID:
+            lbLatencyNs = bb.getLong();
+            break;
+          case SERVER_STATS_SERVICE_LATENCY_ID:
+            serviceLatencyNs = bb.getLong();
+            break;
+          case SERVER_STATS_TRACE_OPTION_ID:
+            traceOption = bb.get();
+            break;
+        }
+      }
+    }
+    try {
+      return ServerStats.create(lbLatencyNs, serviceLatencyNs, traceOption);
+    } catch (IllegalArgumentException e) {
+      throw new ServerStatsDeserializationException(
+          "Serialized ServiceStats contains invalid values: " + e.getMessage());
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/common/ServerStatsFieldEnums.java b/api/src/main/java/io/opencensus/common/ServerStatsFieldEnums.java
new file mode 100644
index 0000000..ff3cfda
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ServerStatsFieldEnums.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2018, 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.common;
+
+import java.util.TreeMap;
+import javax.annotation.Nullable;
+
+/**
+ * A Enum representation for Ids and Size for attributes of {@code ServerStats}.
+ *
+ * <p>See <a
+ * href="https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/CensusServerStatsEncoding.md">opencensus-server-stats-specs</a>
+ * for the field ids and their length defined for Server Stats
+ *
+ * @since 0.16
+ */
+public final class ServerStatsFieldEnums {
+
+  /**
+   * Available Ids for {@code ServerStats} attributes.
+   *
+   * @since 0.16
+   */
+  public enum Id {
+    /**
+     * Id for Latency observed at Load Balancer.
+     *
+     * @since 0.16
+     */
+    SERVER_STATS_LB_LATENCY_ID(0),
+    /**
+     * Id for Latency observed at Server.
+     *
+     * @since 0.16
+     */
+    SERVER_STATS_SERVICE_LATENCY_ID(1),
+    /**
+     * Id for Trace options.
+     *
+     * @since 0.16
+     */
+    SERVER_STATS_TRACE_OPTION_ID(2);
+
+    private final int value;
+
+    private Id(int value) {
+      this.value = value;
+    }
+
+    /**
+     * Returns the numerical value of the {@link Id}.
+     *
+     * @return the numerical value of the {@code Id}.
+     * @since 0.16
+     */
+    public int value() {
+      return value;
+    }
+
+    private static final TreeMap<Integer, Id> map = new TreeMap<Integer, Id>();
+
+    static {
+      for (Id id : Id.values()) {
+        map.put(id.value, id);
+      }
+    }
+
+    /**
+     * Returns the {@link Id} representing the value value of the id.
+     *
+     * @param value integer value for which {@code Id} is being requested.
+     * @return the numerical value of the id. null if the id is not valid
+     * @since 0.16
+     */
+    @Nullable
+    public static Id valueOf(int value) {
+      return map.get(value);
+    }
+  }
+
+  /**
+   * Size for each attributes in {@code ServerStats}.
+   *
+   * @since 0.16
+   */
+  public enum Size {
+    /**
+     * Number of bytes used to represent latency observed at Load Balancer.
+     *
+     * @since 0.16
+     */
+    SERVER_STATS_LB_LATENCY_SIZE(8),
+    /**
+     * Number of bytes used to represent latency observed at Server.
+     *
+     * @since 0.16
+     */
+    SERVER_STATS_SERVICE_LATENCY_SIZE(8),
+    /**
+     * Number of bytes used to represent Trace option.
+     *
+     * @since 0.16
+     */
+    SERVER_STATS_TRACE_OPTION_SIZE(1);
+
+    private final int value;
+
+    private Size(int value) {
+      this.value = value;
+    }
+
+    /**
+     * Returns the numerical value of the {@link Size}.
+     *
+     * @return the numerical value of the {@code Size}.
+     * @since 0.16
+     */
+    public int value() {
+      return value;
+    }
+  }
+
+  private static final int TOTALSIZE = computeTotalSize();
+
+  private ServerStatsFieldEnums() {}
+
+  private static int computeTotalSize() {
+    int sum = 0;
+    for (Size sizeValue : Size.values()) {
+      sum += sizeValue.value();
+      sum += 1; // For Id
+    }
+    return sum;
+  }
+
+  /**
+   * Returns the total size required to encode the {@code ServerStats}.
+   *
+   * @return the total size required to encode all fields in {@code ServerStats}.
+   * @since 0.16
+   */
+  public static int getTotalSize() {
+    return TOTALSIZE;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/common/TimeUtils.java b/api/src/main/java/io/opencensus/common/TimeUtils.java
new file mode 100644
index 0000000..db119e2
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/TimeUtils.java
@@ -0,0 +1,59 @@
+/*
+ * 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.common;
+
+import java.math.BigInteger;
+
+/** Util class for {@link Timestamp} and {@link Duration}. */
+final class TimeUtils {
+  static final long MAX_SECONDS = 315576000000L;
+  static final int MAX_NANOS = 999999999;
+  static final long MILLIS_PER_SECOND = 1000L;
+  static final long NANOS_PER_MILLI = 1000 * 1000;
+  static final long NANOS_PER_SECOND = NANOS_PER_MILLI * MILLIS_PER_SECOND;
+
+  private TimeUtils() {}
+
+  /**
+   * Compares two longs. This functionality is provided by {@code Long.compare(long, long)} in Java
+   * 7.
+   */
+  static int compareLongs(long x, long y) {
+    if (x < y) {
+      return -1;
+    } else if (x == y) {
+      return 0;
+    } else {
+      return 1;
+    }
+  }
+
+  private static final BigInteger MAX_LONG_VALUE = BigInteger.valueOf(Long.MAX_VALUE);
+  private static final BigInteger MIN_LONG_VALUE = BigInteger.valueOf(Long.MIN_VALUE);
+
+  /**
+   * Adds two longs and throws an {@link ArithmeticException} if the result overflows. This
+   * functionality is provided by {@code Math.addExact(long, long)} in Java 8.
+   */
+  static long checkedAdd(long x, long y) {
+    BigInteger sum = BigInteger.valueOf(x).add(BigInteger.valueOf(y));
+    if (sum.compareTo(MAX_LONG_VALUE) > 0 || sum.compareTo(MIN_LONG_VALUE) < 0) {
+      throw new ArithmeticException("Long sum overflow: x=" + x + ", y=" + y);
+    }
+    return x + y;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/common/Timestamp.java b/api/src/main/java/io/opencensus/common/Timestamp.java
new file mode 100644
index 0000000..d17b3fd
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Timestamp.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2016-17, 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.common;
+
+import static io.opencensus.common.TimeUtils.MAX_NANOS;
+import static io.opencensus.common.TimeUtils.MAX_SECONDS;
+import static io.opencensus.common.TimeUtils.MILLIS_PER_SECOND;
+import static io.opencensus.common.TimeUtils.NANOS_PER_MILLI;
+import static io.opencensus.common.TimeUtils.NANOS_PER_SECOND;
+
+import com.google.auto.value.AutoValue;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A representation of an instant in time. The instant is the number of nanoseconds after the number
+ * of seconds since the Unix Epoch.
+ *
+ * <p>Use {@code Tracing.getClock().now()} to get the current timestamp since epoch
+ * (1970-01-01T00:00:00Z).
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class Timestamp implements Comparable<Timestamp> {
+
+  Timestamp() {}
+
+  /**
+   * Creates a new timestamp from given seconds and nanoseconds.
+   *
+   * @param seconds Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must be
+   *     from from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.
+   * @param nanos Non-negative fractions of a second at nanosecond resolution. Negative second
+   *     values with fractions must still have non-negative nanos values that count forward in time.
+   *     Must be from 0 to 999,999,999 inclusive.
+   * @return new {@code Timestamp} with specified fields.
+   * @throws IllegalArgumentException if the arguments are out of range.
+   * @since 0.5
+   */
+  public static Timestamp create(long seconds, int nanos) {
+    if (seconds < -MAX_SECONDS) {
+      throw new IllegalArgumentException(
+          "'seconds' is less than minimum (" + -MAX_SECONDS + "): " + seconds);
+    }
+    if (seconds > MAX_SECONDS) {
+      throw new IllegalArgumentException(
+          "'seconds' is greater than maximum (" + MAX_SECONDS + "): " + seconds);
+    }
+    if (nanos < 0) {
+      throw new IllegalArgumentException("'nanos' is less than zero: " + nanos);
+    }
+    if (nanos > MAX_NANOS) {
+      throw new IllegalArgumentException(
+          "'nanos' is greater than maximum (" + MAX_NANOS + "): " + nanos);
+    }
+    return new AutoValue_Timestamp(seconds, nanos);
+  }
+
+  /**
+   * Creates a new timestamp from the given milliseconds.
+   *
+   * @param epochMilli the timestamp represented in milliseconds since epoch.
+   * @return new {@code Timestamp} with specified fields.
+   * @throws IllegalArgumentException if the number of milliseconds is out of the range that can be
+   *     represented by {@code Timestamp}.
+   * @since 0.5
+   */
+  public static Timestamp fromMillis(long epochMilli) {
+    long secs = floorDiv(epochMilli, MILLIS_PER_SECOND);
+    int mos = (int) floorMod(epochMilli, MILLIS_PER_SECOND);
+    return create(secs, (int) (mos * NANOS_PER_MILLI)); // Safe int * NANOS_PER_MILLI
+  }
+
+  /**
+   * Returns the number of seconds since the Unix Epoch represented by this timestamp.
+   *
+   * @return the number of seconds since the Unix Epoch.
+   * @since 0.5
+   */
+  public abstract long getSeconds();
+
+  /**
+   * Returns the number of nanoseconds after the number of seconds since the Unix Epoch represented
+   * by this timestamp.
+   *
+   * @return the number of nanoseconds after the number of seconds since the Unix Epoch.
+   * @since 0.5
+   */
+  public abstract int getNanos();
+
+  /**
+   * Returns a {@code Timestamp} calculated as this {@code Timestamp} plus some number of
+   * nanoseconds.
+   *
+   * @param nanosToAdd the nanos to add, positive or negative.
+   * @return the calculated {@code Timestamp}. For invalid inputs, a {@code Timestamp} of zero is
+   *     returned.
+   * @throws ArithmeticException if numeric overflow occurs.
+   * @since 0.5
+   */
+  public Timestamp addNanos(long nanosToAdd) {
+    return plus(0, nanosToAdd);
+  }
+
+  /**
+   * Returns a {@code Timestamp} calculated as this {@code Timestamp} plus some {@code Duration}.
+   *
+   * @param duration the {@code Duration} to add.
+   * @return a {@code Timestamp} with the specified {@code Duration} added.
+   * @since 0.5
+   */
+  public Timestamp addDuration(Duration duration) {
+    return plus(duration.getSeconds(), duration.getNanos());
+  }
+
+  /**
+   * Returns a {@link Duration} calculated as: {@code this - timestamp}.
+   *
+   * @param timestamp the {@code Timestamp} to subtract.
+   * @return the calculated {@code Duration}. For invalid inputs, a {@code Duration} of zero is
+   *     returned.
+   * @since 0.5
+   */
+  public Duration subtractTimestamp(Timestamp timestamp) {
+    long durationSeconds = getSeconds() - timestamp.getSeconds();
+    int durationNanos = getNanos() - timestamp.getNanos();
+    if (durationSeconds < 0 && durationNanos > 0) {
+      durationSeconds += 1;
+      durationNanos = (int) (durationNanos - NANOS_PER_SECOND);
+    } else if (durationSeconds > 0 && durationNanos < 0) {
+      durationSeconds -= 1;
+      durationNanos = (int) (durationNanos + NANOS_PER_SECOND);
+    }
+    return Duration.create(durationSeconds, durationNanos);
+  }
+
+  /**
+   * Compares this {@code Timestamp} to the specified {@code Timestamp}.
+   *
+   * @param otherTimestamp the other {@code Timestamp} to compare to, not {@code null}.
+   * @return the comparator value: zero if equal, negative if this timestamp happens before
+   *     otherTimestamp, positive if after.
+   * @throws NullPointerException if otherTimestamp is {@code null}.
+   */
+  @Override
+  public int compareTo(Timestamp otherTimestamp) {
+    int cmp = TimeUtils.compareLongs(getSeconds(), otherTimestamp.getSeconds());
+    if (cmp != 0) {
+      return cmp;
+    }
+    return TimeUtils.compareLongs(getNanos(), otherTimestamp.getNanos());
+  }
+
+  // Returns a Timestamp with the specified duration added.
+  private Timestamp plus(long secondsToAdd, long nanosToAdd) {
+    if ((secondsToAdd | nanosToAdd) == 0) {
+      return this;
+    }
+    long epochSec = TimeUtils.checkedAdd(getSeconds(), secondsToAdd);
+    epochSec = TimeUtils.checkedAdd(epochSec, nanosToAdd / NANOS_PER_SECOND);
+    nanosToAdd = nanosToAdd % NANOS_PER_SECOND;
+    long nanoAdjustment = getNanos() + nanosToAdd; // safe int + NANOS_PER_SECOND
+    return ofEpochSecond(epochSec, nanoAdjustment);
+  }
+
+  // Returns a Timestamp calculated using seconds from the epoch and nanosecond fraction of
+  // second (arbitrary number of nanoseconds).
+  private static Timestamp ofEpochSecond(long epochSecond, long nanoAdjustment) {
+    long secs = TimeUtils.checkedAdd(epochSecond, floorDiv(nanoAdjustment, NANOS_PER_SECOND));
+    int nos = (int) floorMod(nanoAdjustment, NANOS_PER_SECOND);
+    return create(secs, nos);
+  }
+
+  // Returns the result of dividing x by y rounded using floor.
+  private static long floorDiv(long x, long y) {
+    return BigDecimal.valueOf(x).divide(BigDecimal.valueOf(y), 0, RoundingMode.FLOOR).longValue();
+  }
+
+  // Returns the floor modulus "x - (floorDiv(x, y) * y)"
+  private static long floorMod(long x, long y) {
+    return x - floorDiv(x, y) * y;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/common/ToDoubleFunction.java b/api/src/main/java/io/opencensus/common/ToDoubleFunction.java
new file mode 100644
index 0000000..6ace2f7
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ToDoubleFunction.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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.common;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Represents a function that produces a double-valued result. See {@link
+ * io.opencensus.metrics.MetricRegistry} for an example of its use.
+ *
+ * <p>Note: This class is based on the java.util.ToDoubleFunction class added in Java 1.8. We cannot
+ * use the Function from Java 1.8 because this library is Java 1.6 compatible.
+ *
+ * @since 0.16
+ */
+public interface ToDoubleFunction</*@Nullable*/ T> {
+
+  /**
+   * Applies this function to the given argument.
+   *
+   * @param value the function argument.
+   * @return the function result.
+   */
+  double applyAsDouble(/*@Nullable*/ T value);
+}
diff --git a/api/src/main/java/io/opencensus/common/ToLongFunction.java b/api/src/main/java/io/opencensus/common/ToLongFunction.java
new file mode 100644
index 0000000..cd2b68e
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ToLongFunction.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018, 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.common;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Represents a function that produces a long-valued result. See {@link
+ * io.opencensus.metrics.MetricRegistry} for an example of its use.
+ *
+ * <p>Note: This class is based on the java.util.ToLongFunction class added in Java 1.8. We cannot
+ * use the Function from Java 1.8 because this library is Java 1.6 compatible.
+ *
+ * @since 0.16
+ */
+public interface ToLongFunction</*@Nullable*/ T> {
+  /**
+   * Applies this function to the given argument.
+   *
+   * @param value the function argument.
+   * @return the function result.
+   */
+  long applyAsLong(/*@Nullable*/ T value);
+}
diff --git a/api/src/main/java/io/opencensus/common/package-info.java b/api/src/main/java/io/opencensus/common/package-info.java
new file mode 100644
index 0000000..1ebfd7c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/** Common API between different packages in this artifact. */
+package io.opencensus.common;
diff --git a/api/src/main/java/io/opencensus/internal/DefaultVisibilityForTesting.java b/api/src/main/java/io/opencensus/internal/DefaultVisibilityForTesting.java
new file mode 100644
index 0000000..e90a657
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/DefaultVisibilityForTesting.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018, 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.internal;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that an element is package-private instead of private only for the purpose of testing.
+ * This annotation is only meant to be used as documentation in the source code.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target({
+  ElementType.ANNOTATION_TYPE,
+  ElementType.CONSTRUCTOR,
+  ElementType.FIELD,
+  ElementType.METHOD,
+  ElementType.PACKAGE,
+  ElementType.TYPE
+})
+public @interface DefaultVisibilityForTesting {}
diff --git a/api/src/main/java/io/opencensus/internal/NoopScope.java b/api/src/main/java/io/opencensus/internal/NoopScope.java
new file mode 100644
index 0000000..f4a8da0
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/NoopScope.java
@@ -0,0 +1,38 @@
+/*
+ * 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.internal;
+
+import io.opencensus.common.Scope;
+
+/** A {@link Scope} that does nothing when it is created or closed. */
+public final class NoopScope implements Scope {
+  private static final Scope INSTANCE = new NoopScope();
+
+  private NoopScope() {}
+
+  /**
+   * Returns a {@code NoopScope}.
+   *
+   * @return a {@code NoopScope}.
+   */
+  public static Scope getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public void close() {}
+}
diff --git a/api/src/main/java/io/opencensus/internal/Provider.java b/api/src/main/java/io/opencensus/internal/Provider.java
new file mode 100644
index 0000000..8cfb729
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/Provider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016-17, 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.internal;
+
+import java.util.ServiceConfigurationError;
+
+/**
+ * OpenCensus service provider mechanism.
+ *
+ * <pre>{@code
+ * // Initialize a variable using reflection.
+ * foo = Provider.createInstance(
+ *     Class.forName("FooImpl", true, classLoader), Foo.class);
+ * }</pre>
+ */
+public final class Provider {
+  private Provider() {}
+
+  /**
+   * Tries to create an instance of the given rawClass as a subclass of the given superclass.
+   *
+   * @param rawClass The class that is initialized.
+   * @param superclass The initialized class must be a subclass of this.
+   * @return an instance of the class given rawClass which is a subclass of the given superclass.
+   * @throws ServiceConfigurationError if any error happens.
+   */
+  public static <T> T createInstance(Class<?> rawClass, Class<T> superclass) {
+    try {
+      return rawClass.asSubclass(superclass).getConstructor().newInstance();
+    } catch (Exception e) {
+      throw new ServiceConfigurationError(
+          "Provider " + rawClass.getName() + " could not be instantiated.", e);
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/internal/StringUtils.java b/api/src/main/java/io/opencensus/internal/StringUtils.java
new file mode 100644
index 0000000..717e333
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/StringUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016-17, 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.internal;
+
+/** Internal utility methods for working with tag keys, tag values, and metric names. */
+public final class StringUtils {
+
+  /**
+   * Determines whether the {@code String} contains only printable characters.
+   *
+   * @param str the {@code String} to be validated.
+   * @return whether the {@code String} contains only printable characters.
+   */
+  public static boolean isPrintableString(String str) {
+    for (int i = 0; i < str.length(); i++) {
+      if (!isPrintableChar(str.charAt(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static boolean isPrintableChar(char ch) {
+    return ch >= ' ' && ch <= '~';
+  }
+
+  private StringUtils() {}
+}
diff --git a/api/src/main/java/io/opencensus/internal/Utils.java b/api/src/main/java/io/opencensus/internal/Utils.java
new file mode 100644
index 0000000..df5c984
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/Utils.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2018, 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.internal;
+
+import java.util.List;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.NonNull;
+*/
+
+/** General internal utility methods. */
+public final class Utils {
+
+  private Utils() {}
+
+  /**
+   * Throws an {@link IllegalArgumentException} if the argument is false. This method is similar to
+   * {@code Preconditions.checkArgument(boolean, Object)} from Guava.
+   *
+   * @param isValid whether the argument check passed.
+   * @param errorMessage the message to use for the exception. Will be converted to a string using
+   *     {@link String#valueOf(Object)}.
+   */
+  public static void checkArgument(
+      boolean isValid, @javax.annotation.Nullable Object errorMessage) {
+    if (!isValid) {
+      throw new IllegalArgumentException(String.valueOf(errorMessage));
+    }
+  }
+
+  /**
+   * Throws an {@link IllegalArgumentException} if the argument is false. This method is similar to
+   * {@code Preconditions.checkArgument(boolean, Object)} from Guava.
+   *
+   * @param expression a boolean expression
+   * @param errorMessageTemplate a template for the exception message should the check fail. The
+   *     message is formed by replacing each {@code %s} placeholder in the template with an
+   *     argument. These are matched by position - the first {@code %s} gets {@code
+   *     errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message in
+   *     square braces. Unmatched placeholders will be left as-is.
+   * @param errorMessageArgs the arguments to be substituted into the message template. Arguments
+   *     are converted to strings using {@link String#valueOf(Object)}.
+   * @throws IllegalArgumentException if {@code expression} is false
+   * @throws NullPointerException if the check fails and either {@code errorMessageTemplate} or
+   *     {@code errorMessageArgs} is null (don't let this happen)
+   */
+  public static void checkArgument(
+      boolean expression,
+      String errorMessageTemplate,
+      @javax.annotation.Nullable Object... errorMessageArgs) {
+    if (!expression) {
+      throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs));
+    }
+  }
+
+  /**
+   * Throws an {@link IllegalStateException} if the argument is false. This method is similar to
+   * {@code Preconditions.checkState(boolean, Object)} from Guava.
+   *
+   * @param isValid whether the state check passed.
+   * @param errorMessage the message to use for the exception. Will be converted to a string using
+   *     {@link String#valueOf(Object)}.
+   */
+  public static void checkState(boolean isValid, @javax.annotation.Nullable Object errorMessage) {
+    if (!isValid) {
+      throw new IllegalStateException(String.valueOf(errorMessage));
+    }
+  }
+
+  /**
+   * Validates an index in an array or other container. This method throws an {@link
+   * IllegalArgumentException} if the size is negative and throws an {@link
+   * IndexOutOfBoundsException} if the index is negative or greater than or equal to the size. This
+   * method is similar to {@code Preconditions.checkElementIndex(int, int)} from Guava.
+   *
+   * @param index the index to validate.
+   * @param size the size of the array or container.
+   */
+  public static void checkIndex(int index, int size) {
+    if (size < 0) {
+      throw new IllegalArgumentException("Negative size: " + size);
+    }
+    if (index < 0 || index >= size) {
+      throw new IndexOutOfBoundsException("Index out of bounds: size=" + size + ", index=" + index);
+    }
+  }
+
+  /**
+   * Throws a {@link NullPointerException} if the argument is null. This method is similar to {@code
+   * Preconditions.checkNotNull(Object, Object)} from Guava.
+   *
+   * @param arg the argument to check for null.
+   * @param errorMessage the message to use for the exception. Will be converted to a string using
+   *     {@link String#valueOf(Object)}.
+   * @return the argument, if it passes the null check.
+   */
+  public static <T /*>>> extends @NonNull Object*/> T checkNotNull(
+      T arg, @javax.annotation.Nullable Object errorMessage) {
+    if (arg == null) {
+      throw new NullPointerException(String.valueOf(errorMessage));
+    }
+    return arg;
+  }
+
+  /**
+   * Throws a {@link NullPointerException} if any of the list elements is null.
+   *
+   * @param list the argument list to check for null.
+   * @param errorMessage the message to use for the exception. Will be converted to a string using
+   *     {@link String#valueOf(Object)}.
+   */
+  public static <T /*>>> extends @NonNull Object*/> void checkListElementNotNull(
+      List<T> list, @javax.annotation.Nullable Object errorMessage) {
+    for (T element : list) {
+      if (element == null) {
+        throw new NullPointerException(String.valueOf(errorMessage));
+      }
+    }
+  }
+
+  /**
+   * Compares two Objects for equality. This functionality is provided by {@code
+   * Objects.equal(Object, Object)} in Java 7.
+   */
+  public static boolean equalsObjects(
+      @javax.annotation.Nullable Object x, @javax.annotation.Nullable Object y) {
+    return x == null ? y == null : x.equals(y);
+  }
+
+  /**
+   * Substitutes each {@code %s} in {@code template} with an argument. These are matched by
+   * position: the first {@code %s} gets {@code args[0]}, etc. If there are more arguments than
+   * placeholders, the unmatched arguments will be appended to the end of the formatted message in
+   * square braces.
+   *
+   * <p>Copied from {@code Preconditions.format(String, Object...)} from Guava
+   *
+   * @param template a non-null string containing 0 or more {@code %s} placeholders.
+   * @param args the arguments to be substituted into the message template. Arguments are converted
+   *     to strings using {@link String#valueOf(Object)}. Arguments can be null.
+   */
+  // Note that this is somewhat-improperly used from Verify.java as well.
+  private static String format(String template, @javax.annotation.Nullable Object... args) {
+    // If no arguments return the template.
+    if (args == null) {
+      return template;
+    }
+
+    // start substituting the arguments into the '%s' placeholders
+    StringBuilder builder = new StringBuilder(template.length() + 16 * args.length);
+    int templateStart = 0;
+    int i = 0;
+    while (i < args.length) {
+      int placeholderStart = template.indexOf("%s", templateStart);
+      if (placeholderStart == -1) {
+        break;
+      }
+      builder.append(template, templateStart, placeholderStart);
+      builder.append(args[i++]);
+      templateStart = placeholderStart + 2;
+    }
+    builder.append(template, templateStart, template.length());
+
+    // if we run out of placeholders, append the extra args in square braces
+    if (i < args.length) {
+      builder.append(" [");
+      builder.append(args[i++]);
+      while (i < args.length) {
+        builder.append(", ");
+        builder.append(args[i++]);
+      }
+      builder.append(']');
+    }
+
+    return builder.toString();
+  }
+}
diff --git a/api/src/main/java/io/opencensus/internal/ZeroTimeClock.java b/api/src/main/java/io/opencensus/internal/ZeroTimeClock.java
new file mode 100644
index 0000000..fda13e9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/ZeroTimeClock.java
@@ -0,0 +1,49 @@
+/*
+ * 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.internal;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.Immutable;
+
+/** A {@link Clock} that always returns 0. */
+@Immutable
+public final class ZeroTimeClock extends Clock {
+  private static final ZeroTimeClock INSTANCE = new ZeroTimeClock();
+  private static final Timestamp ZERO_TIMESTAMP = Timestamp.create(0, 0);
+
+  private ZeroTimeClock() {}
+
+  /**
+   * Returns a {@code ZeroTimeClock}.
+   *
+   * @return a {@code ZeroTimeClock}.
+   */
+  public static ZeroTimeClock getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public Timestamp now() {
+    return ZERO_TIMESTAMP;
+  }
+
+  @Override
+  public long nowNanos() {
+    return 0;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/internal/package-info.java b/api/src/main/java/io/opencensus/internal/package-info.java
new file mode 100644
index 0000000..5dd35b2
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * Interfaces and implementations that are internal to OpenCensus.
+ *
+ * <p>All the content under this package and its subpackages are considered annotated with {@link
+ * io.opencensus.common.Internal}.
+ */
+@io.opencensus.common.Internal
+package io.opencensus.internal;
diff --git a/api/src/main/java/io/opencensus/metrics/DerivedDoubleGauge.java b/api/src/main/java/io/opencensus/metrics/DerivedDoubleGauge.java
new file mode 100644
index 0000000..3aaca15
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/DerivedDoubleGauge.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.internal.Utils;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import javax.annotation.concurrent.ThreadSafe;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Derived Double Gauge metric, to report instantaneous measurement of a double value. Gauges can go
+ * both up and down. The gauges values can be negative.
+ *
+ * <p>Example: Create a Gauge with an object and a callback function.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ *   private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ *   List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ *   List<LabelValue> labelValues = Arrays.asList(LabelValue.create("Inbound"));
+ *
+ *   DerivedDoubleGauge gauge = metricRegistry.addDerivedDoubleGauge(
+ *       "queue_size", "Pending jobs in a queue", "1", labelKeys);
+ *
+ *   QueueManager queueManager = new QueueManager();
+ *   gauge.createTimeSeries(labelValues, queueManager,
+ *         new ToDoubleFunction<QueueManager>() {
+ *           {@literal @}Override
+ *           public double applyAsDouble(QueueManager queue) {
+ *             return queue.size();
+ *           }
+ *         });
+ *
+ *   void doWork() {
+ *      // Your code here.
+ *   }
+ * }
+ *
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public abstract class DerivedDoubleGauge {
+  /**
+   * Creates a {@code TimeSeries}. The value of a single point in the TimeSeries is observed from a
+   * callback function. This function is invoked whenever metrics are collected, meaning the
+   * reported value is up-to-date. It keeps a {@link WeakReference} to the object and it is the
+   * user's responsibility to manage the lifetime of the object.
+   *
+   * @param labelValues the list of label values.
+   * @param obj the state object from which the function derives a measurement.
+   * @param function the function to be called.
+   * @param <T> the type of the object upon which the function derives a measurement.
+   * @throws NullPointerException if {@code labelValues} is null OR any element of {@code
+   *     labelValues} is null OR {@code function} is null.
+   * @throws IllegalArgumentException if different time series with the same labels already exists
+   *     OR if number of {@code labelValues}s are not equal to the label keys.
+   * @since 0.17
+   */
+  public abstract <T> void createTimeSeries(
+      List<LabelValue> labelValues,
+      /*@Nullable*/ T obj,
+      ToDoubleFunction</*@Nullable*/ T> function);
+
+  /**
+   * Removes the {@code TimeSeries} from the gauge metric, if it is present.
+   *
+   * @param labelValues the list of label values.
+   * @throws NullPointerException if {@code labelValues} is null.
+   * @since 0.17
+   */
+  public abstract void removeTimeSeries(List<LabelValue> labelValues);
+
+  /**
+   * Removes all {@code TimeSeries} from the gauge metric.
+   *
+   * @since 0.17
+   */
+  public abstract void clear();
+
+  /**
+   * Returns the no-op implementation of the {@code DerivedDoubleGauge}.
+   *
+   * @return the no-op implementation of the {@code DerivedDoubleGauge}.
+   * @since 0.17
+   */
+  static DerivedDoubleGauge newNoopDerivedDoubleGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys) {
+    return NoopDerivedDoubleGauge.create(name, description, unit, labelKeys);
+  }
+
+  /** No-op implementations of DerivedDoubleGauge class. */
+  private static final class NoopDerivedDoubleGauge extends DerivedDoubleGauge {
+    private final int labelKeysSize;
+
+    static NoopDerivedDoubleGauge create(
+        String name, String description, String unit, List<LabelKey> labelKeys) {
+      return new NoopDerivedDoubleGauge(name, description, unit, labelKeys);
+    }
+
+    /** Creates a new {@code NoopDerivedDoubleGauge}. */
+    NoopDerivedDoubleGauge(String name, String description, String unit, List<LabelKey> labelKeys) {
+      Utils.checkNotNull(name, "name");
+      Utils.checkNotNull(description, "description");
+      Utils.checkNotNull(unit, "unit");
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+      labelKeysSize = labelKeys.size();
+    }
+
+    @Override
+    public <T> void createTimeSeries(
+        List<LabelValue> labelValues,
+        /*@Nullable*/ T obj,
+        ToDoubleFunction</*@Nullable*/ T> function) {
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+      Utils.checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+      Utils.checkNotNull(function, "function");
+    }
+
+    @Override
+    public void removeTimeSeries(List<LabelValue> labelValues) {
+      Utils.checkNotNull(labelValues, "labelValues");
+    }
+
+    @Override
+    public void clear() {}
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/DerivedLongGauge.java b/api/src/main/java/io/opencensus/metrics/DerivedLongGauge.java
new file mode 100644
index 0000000..621873f
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/DerivedLongGauge.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.internal.Utils;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import javax.annotation.concurrent.ThreadSafe;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Derived Long Gauge metric, to report instantaneous measurement of an int64 value. Gauges can go
+ * both up and down. The gauges values can be negative.
+ *
+ * <p>Example: Create a Gauge with an object and a callback function.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ *   private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ *   List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ *   List<LabelValue> labelValues = Arrays.asList(LabelValue.create("Inbound"));
+ *
+ *   DerivedLongGauge gauge = metricRegistry.addDerivedLongGauge(
+ *       "queue_size", "Pending jobs in a queue", "1", labelKeys);
+ *
+ *   QueueManager queueManager = new QueueManager();
+ *   gauge.createTimeSeries(labelValues, queueManager,
+ *         new ToLongFunction<QueueManager>() {
+ *           {@literal @}Override
+ *           public long applyAsLong(QueueManager queue) {
+ *             return queue.size();
+ *           }
+ *         });
+ *
+ *   void doWork() {
+ *      // Your code here.
+ *   }
+ * }
+ *
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public abstract class DerivedLongGauge {
+  /**
+   * Creates a {@code TimeSeries}. The value of a single point in the TimeSeries is observed from a
+   * callback function. This function is invoked whenever metrics are collected, meaning the
+   * reported value is up-to-date. It keeps a {@link WeakReference} to the object and it is the
+   * user's responsibility to manage the lifetime of the object.
+   *
+   * @param labelValues the list of label values.
+   * @param obj the state object from which the function derives a measurement.
+   * @param function the function to be called.
+   * @param <T> the type of the object upon which the function derives a measurement.
+   * @throws NullPointerException if {@code labelValues} is null OR any element of {@code
+   *     labelValues} is null OR {@code function} is null.
+   * @throws IllegalArgumentException if different time series with the same labels already exists
+   *     OR if number of {@code labelValues}s are not equal to the label keys.
+   * @since 0.17
+   */
+  public abstract <T> void createTimeSeries(
+      List<LabelValue> labelValues, /*@Nullable*/ T obj, ToLongFunction</*@Nullable*/ T> function);
+
+  /**
+   * Removes the {@code TimeSeries} from the gauge metric, if it is present.
+   *
+   * @param labelValues the list of label values.
+   * @throws NullPointerException if {@code labelValues} is null.
+   * @since 0.17
+   */
+  public abstract void removeTimeSeries(List<LabelValue> labelValues);
+
+  /**
+   * Removes all {@code TimeSeries} from the gauge metric.
+   *
+   * @since 0.17
+   */
+  public abstract void clear();
+
+  /**
+   * Returns the no-op implementation of the {@code DerivedLongGauge}.
+   *
+   * @return the no-op implementation of the {@code DerivedLongGauge}.
+   * @since 0.17
+   */
+  static DerivedLongGauge newNoopDerivedLongGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys) {
+    return NoopDerivedLongGauge.create(name, description, unit, labelKeys);
+  }
+
+  /** No-op implementations of DerivedLongGauge class. */
+  private static final class NoopDerivedLongGauge extends DerivedLongGauge {
+    private final int labelKeysSize;
+
+    static NoopDerivedLongGauge create(
+        String name, String description, String unit, List<LabelKey> labelKeys) {
+      return new NoopDerivedLongGauge(name, description, unit, labelKeys);
+    }
+
+    /** Creates a new {@code NoopDerivedLongGauge}. */
+    NoopDerivedLongGauge(String name, String description, String unit, List<LabelKey> labelKeys) {
+      Utils.checkNotNull(name, "name");
+      Utils.checkNotNull(description, "description");
+      Utils.checkNotNull(unit, "unit");
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+      labelKeysSize = labelKeys.size();
+    }
+
+    @Override
+    public <T> void createTimeSeries(
+        List<LabelValue> labelValues,
+        /*@Nullable*/ T obj,
+        ToLongFunction</*@Nullable*/ T> function) {
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+      Utils.checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+      Utils.checkNotNull(function, "function");
+    }
+
+    @Override
+    public void removeTimeSeries(List<LabelValue> labelValues) {
+      Utils.checkNotNull(labelValues, "labelValues");
+    }
+
+    @Override
+    public void clear() {}
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/DoubleGauge.java b/api/src/main/java/io/opencensus/metrics/DoubleGauge.java
new file mode 100644
index 0000000..3275997
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/DoubleGauge.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.internal.Utils;
+import java.util.List;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Double Gauge metric, to report instantaneous measurement of a double value. Gauges can go both up
+ * and down. The gauges values can be negative.
+ *
+ * <p>Example 1: Create a Gauge with default labels.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ *   private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ *   List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ *
+ *   DoubleGauge gauge = metricRegistry.addDoubleGauge("queue_size",
+ *                       "Pending jobs", "1", labelKeys);
+ *
+ *   // It is recommended to keep a reference of a point for manual operations.
+ *   DoublePoint defaultPoint = gauge.getDefaultTimeSeries();
+ *
+ *   void doWork() {
+ *      // Your code here.
+ *      defaultPoint.add(10);
+ *   }
+ *
+ * }
+ * }</pre>
+ *
+ * <p>Example 2: You can also use labels(keys and values) to track different types of metric.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ *   private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ *   List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ *   List<LabelValue> labelValues = Arrays.asList(LabelValue.create("Inbound"));
+ *
+ *   DoubleGauge gauge = metricRegistry.addDoubleGauge("queue_size",
+ *                       "Pending jobs", "1", labelKeys);
+ *
+ *   // It is recommended to keep a reference of a point for manual operations.
+ *   DoublePoint inboundPoint = gauge.getOrCreateTimeSeries(labelValues);
+ *
+ *   void doSomeWork() {
+ *      // Your code here.
+ *      inboundPoint.set(15);
+ *   }
+ *
+ * }
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public abstract class DoubleGauge {
+
+  /**
+   * Creates a {@code TimeSeries} and returns a {@code DoublePoint} if the specified {@code
+   * labelValues} is not already associated with this gauge, else returns an existing {@code
+   * DoublePoint}.
+   *
+   * <p>It is recommended to keep a reference to the DoublePoint instead of always calling this
+   * method for manual operations.
+   *
+   * @param labelValues the list of label values. The number of label values must be the same to
+   *     that of the label keys passed to {@link MetricRegistry#addDoubleGauge}.
+   * @return a {@code DoublePoint} the value of single gauge.
+   * @throws NullPointerException if {@code labelValues} is null OR any element of {@code
+   *     labelValues} is null.
+   * @throws IllegalArgumentException if number of {@code labelValues}s are not equal to the label
+   *     keys.
+   * @since 0.17
+   */
+  public abstract DoublePoint getOrCreateTimeSeries(List<LabelValue> labelValues);
+
+  /**
+   * Returns a {@code DoublePoint} for a gauge with all labels not set, or default labels.
+   *
+   * @return a {@code DoublePoint} for a gauge with all labels not set, or default labels.
+   * @since 0.17
+   */
+  public abstract DoublePoint getDefaultTimeSeries();
+
+  /**
+   * Removes the {@code TimeSeries} from the gauge metric, if it is present. i.e. references to
+   * previous {@code DoublePoint} objects are invalid (not part of the metric).
+   *
+   * @param labelValues the list of label values.
+   * @throws NullPointerException if {@code labelValues} is null or any element of {@code
+   *     labelValues} is null.
+   * @since 0.17
+   */
+  public abstract void removeTimeSeries(List<LabelValue> labelValues);
+
+  /**
+   * Removes all {@code TimeSeries} from the gauge metric. i.e. references to all previous {@code
+   * DoublePoint} objects are invalid (not part of the metric).
+   *
+   * @since 0.17
+   */
+  public abstract void clear();
+
+  /**
+   * Returns the no-op implementation of the {@code DoubleGauge}.
+   *
+   * @return the no-op implementation of the {@code DoubleGauge}.
+   * @since 0.17
+   */
+  static DoubleGauge newNoopDoubleGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys) {
+    return NoopDoubleGauge.create(name, description, unit, labelKeys);
+  }
+
+  /**
+   * The value of a single point in the Gauge.TimeSeries.
+   *
+   * @since 0.17
+   */
+  public abstract static class DoublePoint {
+
+    /**
+     * Adds the given value to the current value. The values can be negative.
+     *
+     * @param amt the value to add
+     * @since 0.17
+     */
+    public abstract void add(double amt);
+
+    /**
+     * Sets the given value.
+     *
+     * @param val the new value.
+     * @since 0.17
+     */
+    public abstract void set(double val);
+  }
+
+  /** No-op implementations of DoubleGauge class. */
+  private static final class NoopDoubleGauge extends DoubleGauge {
+    private final int labelKeysSize;
+
+    static NoopDoubleGauge create(
+        String name, String description, String unit, List<LabelKey> labelKeys) {
+      return new NoopDoubleGauge(name, description, unit, labelKeys);
+    }
+
+    /** Creates a new {@code NoopDoublePoint}. */
+    NoopDoubleGauge(String name, String description, String unit, List<LabelKey> labelKeys) {
+      Utils.checkNotNull(name, "name");
+      Utils.checkNotNull(description, "description");
+      Utils.checkNotNull(unit, "unit");
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+      labelKeysSize = labelKeys.size();
+    }
+
+    @Override
+    public NoopDoublePoint getOrCreateTimeSeries(List<LabelValue> labelValues) {
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+      Utils.checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+      return NoopDoublePoint.INSTANCE;
+    }
+
+    @Override
+    public NoopDoublePoint getDefaultTimeSeries() {
+      return NoopDoublePoint.INSTANCE;
+    }
+
+    @Override
+    public void removeTimeSeries(List<LabelValue> labelValues) {
+      Utils.checkNotNull(labelValues, "labelValues");
+    }
+
+    @Override
+    public void clear() {}
+
+    /** No-op implementations of DoublePoint class. */
+    private static final class NoopDoublePoint extends DoublePoint {
+      private static final NoopDoublePoint INSTANCE = new NoopDoublePoint();
+
+      private NoopDoublePoint() {}
+
+      @Override
+      public void add(double amt) {}
+
+      @Override
+      public void set(double val) {}
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/LabelKey.java b/api/src/main/java/io/opencensus/metrics/LabelKey.java
new file mode 100644
index 0000000..efc51e6
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/LabelKey.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The key of a {@code Label} associated with a {@code MetricDescriptor}.
+ *
+ * @since 0.15
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class LabelKey {
+
+  LabelKey() {}
+
+  /**
+   * Creates a {@link LabelKey}.
+   *
+   * @param key the key of a {@code Label}.
+   * @param description a human-readable description of what this label key represents.
+   * @return a {@code LabelKey}.
+   * @since 0.17
+   */
+  public static LabelKey create(String key, String description) {
+    return new AutoValue_LabelKey(key, description);
+  }
+
+  /**
+   * Returns the key of this {@link LabelKey}.
+   *
+   * @return the key.
+   * @since 0.17
+   */
+  public abstract String getKey();
+
+  /**
+   * Returns the description of this {@link LabelKey}.
+   *
+   * @return the description.
+   * @since 0.17
+   */
+  public abstract String getDescription();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/LabelValue.java b/api/src/main/java/io/opencensus/metrics/LabelValue.java
new file mode 100644
index 0000000..e570865
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/LabelValue.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The value of a {@code Label} associated with a {@code TimeSeries}.
+ *
+ * @since 0.15
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class LabelValue {
+
+  LabelValue() {}
+
+  /**
+   * Creates a {@link LabelValue}.
+   *
+   * @param value the value of a {@code Label}. {@code null} value indicates an unset {@code
+   *     LabelValue}.
+   * @return a {@code LabelValue}.
+   * @since 0.17
+   */
+  public static LabelValue create(@Nullable String value) {
+    return new AutoValue_LabelValue(value);
+  }
+
+  /**
+   * Returns the value of this {@link LabelValue}. Returns {@code null} if the value is unset and
+   * supposed to be ignored.
+   *
+   * @return the value.
+   * @since 0.17
+   */
+  @Nullable
+  public abstract String getValue();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/LongGauge.java b/api/src/main/java/io/opencensus/metrics/LongGauge.java
new file mode 100644
index 0000000..1d4489c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/LongGauge.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.internal.Utils;
+import java.util.List;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Long Gauge metric, to report instantaneous measurement of an int64 value. Gauges can go both up
+ * and down. The gauges values can be negative.
+ *
+ * <p>Example 1: Create a Gauge with default labels.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ *   private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ *   List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ *
+ *   LongGauge gauge = metricRegistry.addLongGauge("queue_size", "Pending jobs", "1", labelKeys);
+ *
+ *   // It is recommended to keep a reference of a point for manual operations.
+ *   LongPoint defaultPoint = gauge.getDefaultTimeSeries();
+ *
+ *   void doWork() {
+ *      // Your code here.
+ *      defaultPoint.add(10);
+ *   }
+ *
+ * }
+ * }</pre>
+ *
+ * <p>Example 2: You can also use labels(keys and values) to track different types of metric.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ *   private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ *   List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ *   List<LabelValue> labelValues = Arrays.asList(LabelValue.create("Inbound"));
+ *
+ *   LongGauge gauge = metricRegistry.addLongGauge("queue_size", "Pending jobs", "1", labelKeys);
+ *
+ *   // It is recommended to keep a reference of a point for manual operations.
+ *   LongPoint inboundPoint = gauge.getOrCreateTimeSeries(labelValues);
+ *
+ *   void doSomeWork() {
+ *      // Your code here.
+ *      inboundPoint.set(15);
+ *   }
+ *
+ * }
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public abstract class LongGauge {
+
+  /**
+   * Creates a {@code TimeSeries} and returns a {@code LongPoint} if the specified {@code
+   * labelValues} is not already associated with this gauge, else returns an existing {@code
+   * LongPoint}.
+   *
+   * <p>It is recommended to keep a reference to the LongPoint instead of always calling this method
+   * for manual operations.
+   *
+   * @param labelValues the list of label values. The number of label values must be the same to
+   *     that of the label keys passed to {@link MetricRegistry#addLongGauge}.
+   * @return a {@code LongPoint} the value of single gauge.
+   * @throws NullPointerException if {@code labelValues} is null OR any element of {@code
+   *     labelValues} is null.
+   * @throws IllegalArgumentException if number of {@code labelValues}s are not equal to the label
+   *     keys passed to {@link MetricRegistry#addLongGauge}.
+   * @since 0.17
+   */
+  public abstract LongPoint getOrCreateTimeSeries(List<LabelValue> labelValues);
+
+  /**
+   * Returns a {@code LongPoint} for a gauge with all labels not set, or default labels.
+   *
+   * @return a {@code LongPoint} for a gauge with all labels not set, or default labels.
+   * @since 0.17
+   */
+  public abstract LongPoint getDefaultTimeSeries();
+
+  /**
+   * Removes the {@code TimeSeries} from the gauge metric, if it is present. i.e. references to
+   * previous {@code LongPoint} objects are invalid (not part of the metric).
+   *
+   * @param labelValues the list of label values.
+   * @throws NullPointerException if {@code labelValues} is null.
+   * @since 0.17
+   */
+  public abstract void removeTimeSeries(List<LabelValue> labelValues);
+
+  /**
+   * Removes all {@code TimeSeries} from the gauge metric. i.e. references to all previous {@code
+   * LongPoint} objects are invalid (not part of the metric).
+   *
+   * @since 0.17
+   */
+  public abstract void clear();
+
+  /**
+   * Returns the no-op implementation of the {@code LongGauge}.
+   *
+   * @return the no-op implementation of the {@code LongGauge}.
+   * @since 0.17
+   */
+  static LongGauge newNoopLongGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys) {
+    return NoopLongGauge.create(name, description, unit, labelKeys);
+  }
+
+  /**
+   * The value of a single point in the Gauge.TimeSeries.
+   *
+   * @since 0.17
+   */
+  public abstract static class LongPoint {
+
+    /**
+     * Adds the given value to the current value. The values can be negative.
+     *
+     * @param amt the value to add
+     * @since 0.17
+     */
+    public abstract void add(long amt);
+
+    /**
+     * Sets the given value.
+     *
+     * @param val the new value.
+     * @since 0.17
+     */
+    public abstract void set(long val);
+  }
+
+  /** No-op implementations of LongGauge class. */
+  private static final class NoopLongGauge extends LongGauge {
+    private final int labelKeysSize;
+
+    static NoopLongGauge create(
+        String name, String description, String unit, List<LabelKey> labelKeys) {
+      return new NoopLongGauge(name, description, unit, labelKeys);
+    }
+
+    /** Creates a new {@code NoopLongPoint}. */
+    NoopLongGauge(String name, String description, String unit, List<LabelKey> labelKeys) {
+      labelKeysSize = labelKeys.size();
+    }
+
+    @Override
+    public NoopLongPoint getOrCreateTimeSeries(List<LabelValue> labelValues) {
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+      Utils.checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+      return NoopLongPoint.INSTANCE;
+    }
+
+    @Override
+    public NoopLongPoint getDefaultTimeSeries() {
+      return NoopLongPoint.INSTANCE;
+    }
+
+    @Override
+    public void removeTimeSeries(List<LabelValue> labelValues) {
+      Utils.checkNotNull(labelValues, "labelValues");
+    }
+
+    @Override
+    public void clear() {}
+
+    /** No-op implementations of LongPoint class. */
+    private static final class NoopLongPoint extends LongPoint {
+      private static final NoopLongPoint INSTANCE = new NoopLongPoint();
+
+      private NoopLongPoint() {}
+
+      @Override
+      public void add(long amt) {}
+
+      @Override
+      public void set(long val) {}
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/MetricRegistry.java b/api/src/main/java/io/opencensus/metrics/MetricRegistry.java
new file mode 100644
index 0000000..5be1559
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/MetricRegistry.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.internal.Utils;
+import java.util.List;
+
+/**
+ * Creates and manages your application's set of metrics. The default implementation of this creates
+ * a {@link io.opencensus.metrics.export.MetricProducer} and registers it to the global {@link
+ * io.opencensus.metrics.export.MetricProducerManager}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public abstract class MetricRegistry {
+  /**
+   * Builds a new long gauge to be added to the registry. This is more convenient form when you want
+   * to manually increase and decrease values as per your service requirements.
+   *
+   * @param name the name of the metric.
+   * @param description the description of the metric.
+   * @param unit the unit of the metric.
+   * @param labelKeys the list of the label keys.
+   * @throws NullPointerException if {@code labelKeys} is null OR any element of {@code labelKeys}
+   *     is null OR {@code name}, {@code description}, {@code unit} is null.
+   * @throws IllegalArgumentException if different metric with the same name already registered.
+   * @since 0.17
+   */
+  @ExperimentalApi
+  public abstract LongGauge addLongGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys);
+
+  /**
+   * Builds a new double gauge to be added to the registry. This is more convenient form when you
+   * want to manually increase and decrease values as per your service requirements.
+   *
+   * @param name the name of the metric.
+   * @param description the description of the metric.
+   * @param unit the unit of the metric.
+   * @param labelKeys the list of the label keys.
+   * @throws NullPointerException if {@code labelKeys} is null OR any element of {@code labelKeys}
+   *     is null OR {@code name}, {@code description}, {@code unit} is null.
+   * @throws IllegalArgumentException if different metric with the same name already registered.
+   * @since 0.17
+   */
+  @ExperimentalApi
+  public abstract DoubleGauge addDoubleGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys);
+
+  /**
+   * Builds a new derived long gauge to be added to the registry. This is more convenient form when
+   * you want to define a gauge by executing a {@link ToLongFunction} on an object.
+   *
+   * @param name the name of the metric.
+   * @param description the description of the metric.
+   * @param unit the unit of the metric.
+   * @param labelKeys the list of the label keys.
+   * @throws NullPointerException if {@code labelKeys} is null OR any element of {@code labelKeys}
+   *     is null OR {@code name}, {@code description}, {@code unit} is null.
+   * @throws IllegalArgumentException if different metric with the same name already registered.
+   * @since 0.17
+   */
+  @ExperimentalApi
+  public abstract DerivedLongGauge addDerivedLongGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys);
+
+  /**
+   * Builds a new derived double gauge to be added to the registry. This is more convenient form
+   * when you want to define a gauge by executing a {@link ToDoubleFunction} on an object.
+   *
+   * @param name the name of the metric.
+   * @param description the description of the metric.
+   * @param unit the unit of the metric.
+   * @param labelKeys the list of the label keys.
+   * @throws NullPointerException if {@code labelKeys} is null OR any element of {@code labelKeys}
+   *     is null OR {@code name}, {@code description}, {@code unit} is null.
+   * @throws IllegalArgumentException if different metric with the same name already registered.
+   * @since 0.17
+   */
+  @ExperimentalApi
+  public abstract DerivedDoubleGauge addDerivedDoubleGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys);
+
+  static MetricRegistry newNoopMetricRegistry() {
+    return new NoopMetricRegistry();
+  }
+
+  private static final class NoopMetricRegistry extends MetricRegistry {
+
+    @Override
+    public LongGauge addLongGauge(
+        String name, String description, String unit, List<LabelKey> labelKeys) {
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+      return LongGauge.newNoopLongGauge(
+          Utils.checkNotNull(name, "name"),
+          Utils.checkNotNull(description, "description"),
+          Utils.checkNotNull(unit, "unit"),
+          labelKeys);
+    }
+
+    @Override
+    public DoubleGauge addDoubleGauge(
+        String name, String description, String unit, List<LabelKey> labelKeys) {
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+      return DoubleGauge.newNoopDoubleGauge(
+          Utils.checkNotNull(name, "name"),
+          Utils.checkNotNull(description, "description"),
+          Utils.checkNotNull(unit, "unit"),
+          labelKeys);
+    }
+
+    @Override
+    public DerivedLongGauge addDerivedLongGauge(
+        String name, String description, String unit, List<LabelKey> labelKeys) {
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+      return DerivedLongGauge.newNoopDerivedLongGauge(
+          Utils.checkNotNull(name, "name"),
+          Utils.checkNotNull(description, "description"),
+          Utils.checkNotNull(unit, "unit"),
+          labelKeys);
+    }
+
+    @Override
+    public DerivedDoubleGauge addDerivedDoubleGauge(
+        String name, String description, String unit, List<LabelKey> labelKeys) {
+      Utils.checkListElementNotNull(
+          Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+      return DerivedDoubleGauge.newNoopDerivedDoubleGauge(
+          Utils.checkNotNull(name, "name"),
+          Utils.checkNotNull(description, "description"),
+          Utils.checkNotNull(unit, "unit"),
+          labelKeys);
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/Metrics.java b/api/src/main/java/io/opencensus/metrics/Metrics.java
new file mode 100644
index 0000000..920a4a8
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/Metrics.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Provider;
+import io.opencensus.metrics.export.ExportComponent;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * Class for accessing the default {@link MetricsComponent}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public final class Metrics {
+  private static final Logger logger = Logger.getLogger(Metrics.class.getName());
+  private static final MetricsComponent metricsComponent =
+      loadMetricsComponent(MetricsComponent.class.getClassLoader());
+
+  /**
+   * Returns the global {@link ExportComponent}.
+   *
+   * @return the global {@code ExportComponent}.
+   * @since 0.17
+   */
+  public static ExportComponent getExportComponent() {
+    return metricsComponent.getExportComponent();
+  }
+
+  /**
+   * Returns the global {@link MetricRegistry}.
+   *
+   * <p>This {@code MetricRegistry} is already added to the global {@link
+   * io.opencensus.metrics.export.MetricProducerManager}.
+   *
+   * @return the global {@code MetricRegistry}.
+   * @since 0.17
+   */
+  public static MetricRegistry getMetricRegistry() {
+    return metricsComponent.getMetricRegistry();
+  }
+
+  // Any provider that may be used for MetricsComponent can be added here.
+  @DefaultVisibilityForTesting
+  static MetricsComponent loadMetricsComponent(@Nullable ClassLoader classLoader) {
+    try {
+      // Call Class.forName with literal string name of the class to help shading tools.
+      return Provider.createInstance(
+          Class.forName(
+              "io.opencensus.impl.metrics.MetricsComponentImpl", /*initialize=*/ true, classLoader),
+          MetricsComponent.class);
+    } catch (ClassNotFoundException e) {
+      logger.log(
+          Level.FINE,
+          "Couldn't load full implementation for MetricsComponent, now trying to load lite "
+              + "implementation.",
+          e);
+    }
+    try {
+      // Call Class.forName with literal string name of the class to help shading tools.
+      return Provider.createInstance(
+          Class.forName(
+              "io.opencensus.impllite.metrics.MetricsComponentImplLite",
+              /*initialize=*/ true,
+              classLoader),
+          MetricsComponent.class);
+    } catch (ClassNotFoundException e) {
+      logger.log(
+          Level.FINE,
+          "Couldn't load lite implementation for MetricsComponent, now using default "
+              + "implementation for MetricsComponent.",
+          e);
+    }
+    return MetricsComponent.newNoopMetricsComponent();
+  }
+
+  private Metrics() {}
+}
diff --git a/api/src/main/java/io/opencensus/metrics/MetricsComponent.java b/api/src/main/java/io/opencensus/metrics/MetricsComponent.java
new file mode 100644
index 0000000..3a99230
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/MetricsComponent.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.metrics.export.ExportComponent;
+
+/**
+ * Class that holds the implementation instance for {@link ExportComponent}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public abstract class MetricsComponent {
+
+  /**
+   * Returns the {@link ExportComponent} with the provided implementation. If no implementation is
+   * provided then no-op implementations will be used.
+   *
+   * @return the {@link ExportComponent} implementation.
+   * @since 0.17
+   */
+  public abstract ExportComponent getExportComponent();
+
+  /**
+   * Returns the {@link MetricRegistry} with the provided implementation.
+   *
+   * @return the {@link MetricRegistry} implementation.
+   * @since 0.17
+   */
+  public abstract MetricRegistry getMetricRegistry();
+
+  /**
+   * Returns an instance that contains no-op implementations for all the instances.
+   *
+   * @return an instance that contains no-op implementations for all the instances.
+   */
+  static MetricsComponent newNoopMetricsComponent() {
+    return new NoopMetricsComponent();
+  }
+
+  private static final class NoopMetricsComponent extends MetricsComponent {
+    private static final ExportComponent EXPORT_COMPONENT =
+        ExportComponent.newNoopExportComponent();
+    private static final MetricRegistry METRIC_REGISTRY = MetricRegistry.newNoopMetricRegistry();
+
+    @Override
+    public ExportComponent getExportComponent() {
+      return EXPORT_COMPONENT;
+    }
+
+    @Override
+    public MetricRegistry getMetricRegistry() {
+      return METRIC_REGISTRY;
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Distribution.java b/api/src/main/java/io/opencensus/metrics/export/Distribution.java
new file mode 100644
index 0000000..d55f101
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Distribution.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.Function;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link Distribution} contains summary statistics for a population of values. It optionally
+ * contains a histogram representing the distribution of those values across a set of buckets.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@AutoValue
+@Immutable
+public abstract class Distribution {
+
+  Distribution() {}
+
+  /**
+   * Creates a {@link Distribution}.
+   *
+   * @param count the count of the population values.
+   * @param sum the sum of the population values.
+   * @param sumOfSquaredDeviations the sum of squared deviations of the population values.
+   * @param bucketOptions the bucket options used to create a histogram for the distribution.
+   * @param buckets {@link Bucket}s of a histogram.
+   * @return a {@code Distribution}.
+   * @since 0.17
+   */
+  public static Distribution create(
+      long count,
+      double sum,
+      double sumOfSquaredDeviations,
+      BucketOptions bucketOptions,
+      List<Bucket> buckets) {
+    Utils.checkArgument(count >= 0, "count should be non-negative.");
+    Utils.checkArgument(
+        sumOfSquaredDeviations >= 0, "sum of squared deviations should be non-negative.");
+    if (count == 0) {
+      Utils.checkArgument(sum == 0, "sum should be 0 if count is 0.");
+      Utils.checkArgument(
+          sumOfSquaredDeviations == 0, "sum of squared deviations should be 0 if count is 0.");
+    }
+    Utils.checkNotNull(bucketOptions, "bucketOptions");
+    List<Bucket> bucketsCopy =
+        Collections.unmodifiableList(new ArrayList<Bucket>(Utils.checkNotNull(buckets, "buckets")));
+    Utils.checkListElementNotNull(bucketsCopy, "bucket");
+    return new AutoValue_Distribution(
+        count, sum, sumOfSquaredDeviations, bucketOptions, bucketsCopy);
+  }
+
+  /**
+   * Returns the aggregated count.
+   *
+   * @return the aggregated count.
+   * @since 0.17
+   */
+  public abstract long getCount();
+
+  /**
+   * Returns the aggregated sum.
+   *
+   * @return the aggregated sum.
+   * @since 0.17
+   */
+  public abstract double getSum();
+
+  /**
+   * Returns the aggregated sum of squared deviations.
+   *
+   * <p>The sum of squared deviations from the mean of the values in the population. For values x_i
+   * this is:
+   *
+   * <p>Sum[i=1..n]((x_i - mean)^2)
+   *
+   * <p>If count is zero then this field must be zero.
+   *
+   * @return the aggregated sum of squared deviations.
+   * @since 0.17
+   */
+  public abstract double getSumOfSquaredDeviations();
+
+  /**
+   * Returns bucket options used to create a histogram for the distribution.
+   *
+   * @return the {@code BucketOptions} associated with the {@code Distribution}, or {@code null} if
+   *     there isn't one.
+   * @since 0.17
+   */
+  @Nullable
+  public abstract BucketOptions getBucketOptions();
+
+  /**
+   * Returns the aggregated histogram {@link Bucket}s.
+   *
+   * @return the aggregated histogram buckets.
+   * @since 0.17
+   */
+  public abstract List<Bucket> getBuckets();
+
+  /**
+   * The bucket options used to create a histogram for the distribution.
+   *
+   * @since 0.17
+   */
+  @Immutable
+  public abstract static class BucketOptions {
+
+    private BucketOptions() {}
+
+    /**
+     * Returns a {@link ExplicitOptions}.
+     *
+     * <p>The bucket boundaries for that histogram are described by bucket_bounds. This defines
+     * size(bucket_bounds) + 1 (= N) buckets. The boundaries for bucket index i are:
+     *
+     * <ul>
+     *   <li>{@code [0, bucket_bounds[i]) for i == 0}
+     *   <li>{@code [bucket_bounds[i-1], bucket_bounds[i]) for 0 < i < N-1}
+     *   <li>{@code [bucket_bounds[i-1], +infinity) for i == N-1}
+     * </ul>
+     *
+     * <p>If bucket_bounds has no elements (zero size), then there is no histogram associated with
+     * the Distribution. If bucket_bounds has only one element, there are no finite buckets, and
+     * that single element is the common boundary of the overflow and underflow buckets. The values
+     * must be monotonically increasing.
+     *
+     * @param bucketBoundaries the bucket boundaries of a distribution (given explicitly). The
+     *     values must be strictly increasing and should be positive values.
+     * @return a {@code ExplicitOptions} {@code BucketOptions}.
+     * @since 0.17
+     */
+    public static BucketOptions explicitOptions(List<Double> bucketBoundaries) {
+      return ExplicitOptions.create(bucketBoundaries);
+    }
+
+    /**
+     * Applies the given match function to the underlying BucketOptions.
+     *
+     * @param explicitFunction the function that should be applied if the BucketOptions has type
+     *     {@code ExplicitOptions}.
+     * @param defaultFunction the function that should be applied if the BucketOptions has a type
+     *     that was added after this {@code match} method was added to the API. See {@link
+     *     io.opencensus.common.Functions} for some common functions for handling unknown types.
+     * @return the result of the function applied to the underlying BucketOptions.
+     * @since 0.17
+     */
+    public abstract <T> T match(
+        Function<? super ExplicitOptions, T> explicitFunction,
+        Function<? super BucketOptions, T> defaultFunction);
+
+    /** A Bucket with explicit bounds {@link BucketOptions}. */
+    @AutoValue
+    @Immutable
+    public abstract static class ExplicitOptions extends BucketOptions {
+
+      ExplicitOptions() {}
+
+      @Override
+      public final <T> T match(
+          Function<? super ExplicitOptions, T> explicitFunction,
+          Function<? super BucketOptions, T> defaultFunction) {
+        return explicitFunction.apply(this);
+      }
+
+      /**
+       * Creates a {@link ExplicitOptions}.
+       *
+       * @param bucketBoundaries the bucket boundaries of a distribution (given explicitly). The
+       *     values must be strictly increasing and should be positive.
+       * @return a {@code ExplicitOptions}.
+       * @since 0.17
+       */
+      private static ExplicitOptions create(List<Double> bucketBoundaries) {
+        Utils.checkNotNull(bucketBoundaries, "bucketBoundaries");
+        List<Double> bucketBoundariesCopy =
+            Collections.unmodifiableList(new ArrayList<Double>(bucketBoundaries));
+        checkBucketBoundsAreSorted(bucketBoundariesCopy);
+        return new AutoValue_Distribution_BucketOptions_ExplicitOptions(bucketBoundariesCopy);
+      }
+
+      private static void checkBucketBoundsAreSorted(List<Double> bucketBoundaries) {
+        if (bucketBoundaries.size() >= 1) {
+          double previous = Utils.checkNotNull(bucketBoundaries.get(0), "bucketBoundary");
+          Utils.checkArgument(previous > 0, "bucket boundary should be > 0");
+          for (int i = 1; i < bucketBoundaries.size(); i++) {
+            double next = Utils.checkNotNull(bucketBoundaries.get(i), "bucketBoundary");
+            Utils.checkArgument(previous < next, "bucket boundaries not sorted.");
+            previous = next;
+          }
+        }
+      }
+
+      /**
+       * Returns the bucket boundaries of this distribution.
+       *
+       * @return the bucket boundaries of this distribution.
+       * @since 0.17
+       */
+      public abstract List<Double> getBucketBoundaries();
+    }
+  }
+
+  /**
+   * The histogram bucket of the population values.
+   *
+   * @since 0.17
+   */
+  @AutoValue
+  @Immutable
+  public abstract static class Bucket {
+
+    Bucket() {}
+
+    /**
+     * Creates a {@link Bucket}.
+     *
+     * @param count the number of values in each bucket of the histogram.
+     * @return a {@code Bucket}.
+     * @since 0.17
+     */
+    public static Bucket create(long count) {
+      Utils.checkArgument(count >= 0, "bucket count should be non-negative.");
+      return new AutoValue_Distribution_Bucket(count, null);
+    }
+
+    /**
+     * Creates a {@link Bucket} with an {@link Exemplar}.
+     *
+     * @param count the number of values in each bucket of the histogram.
+     * @param exemplar the {@code Exemplar} of this {@code Bucket}.
+     * @return a {@code Bucket}.
+     * @since 0.17
+     */
+    public static Bucket create(long count, Exemplar exemplar) {
+      Utils.checkArgument(count >= 0, "bucket count should be non-negative.");
+      Utils.checkNotNull(exemplar, "exemplar");
+      return new AutoValue_Distribution_Bucket(count, exemplar);
+    }
+
+    /**
+     * Returns the number of values in each bucket of the histogram.
+     *
+     * @return the number of values in each bucket of the histogram.
+     * @since 0.17
+     */
+    public abstract long getCount();
+
+    /**
+     * Returns the {@link Exemplar} associated with the {@link Bucket}, or {@code null} if there
+     * isn't one.
+     *
+     * @return the {@code Exemplar} associated with the {@code Bucket}, or {@code null} if there
+     *     isn't one.
+     * @since 0.17
+     */
+    @Nullable
+    public abstract Exemplar getExemplar();
+  }
+
+  /**
+   * An example point that may be used to annotate aggregated distribution values, associated with a
+   * histogram bucket.
+   *
+   * @since 0.17
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class Exemplar {
+
+    Exemplar() {}
+
+    /**
+     * Returns value of the {@link Exemplar} point.
+     *
+     * @return value of the {@code Exemplar} point.
+     * @since 0.17
+     */
+    public abstract double getValue();
+
+    /**
+     * Returns the time that this {@link Exemplar}'s value was recorded.
+     *
+     * @return the time that this {@code Exemplar}'s value was recorded.
+     * @since 0.17
+     */
+    public abstract Timestamp getTimestamp();
+
+    /**
+     * Returns the contextual information about the example value, represented as a string map.
+     *
+     * @return the contextual information about the example value.
+     * @since 0.17
+     */
+    public abstract Map<String, String> getAttachments();
+
+    /**
+     * Creates an {@link Exemplar}.
+     *
+     * @param value value of the {@link Exemplar} point.
+     * @param timestamp the time that this {@code Exemplar}'s value was recorded.
+     * @param attachments the contextual information about the example value.
+     * @return an {@code Exemplar}.
+     * @since 0.17
+     */
+    public static Exemplar create(
+        double value, Timestamp timestamp, Map<String, String> attachments) {
+      Utils.checkNotNull(attachments, "attachments");
+      Map<String, String> attachmentsCopy =
+          Collections.unmodifiableMap(new HashMap<String, String>(attachments));
+      for (Entry<String, String> entry : attachmentsCopy.entrySet()) {
+        Utils.checkNotNull(entry.getKey(), "key of attachments");
+        Utils.checkNotNull(entry.getValue(), "value of attachments");
+      }
+      return new AutoValue_Distribution_Exemplar(value, timestamp, attachmentsCopy);
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/ExportComponent.java b/api/src/main/java/io/opencensus/metrics/export/ExportComponent.java
new file mode 100644
index 0000000..11e1fdb
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/ExportComponent.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import io.opencensus.common.ExperimentalApi;
+
+/**
+ * Class that holds the implementation instance for {@link MetricProducerManager}.
+ *
+ * <p>Unless otherwise noted all methods (on component) results are cacheable.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public abstract class ExportComponent {
+  /**
+   * Returns the no-op implementation of the {@code ExportComponent}.
+   *
+   * @return the no-op implementation of the {@code ExportComponent}.
+   * @since 0.17
+   */
+  public static ExportComponent newNoopExportComponent() {
+    return new NoopExportComponent();
+  }
+
+  /**
+   * Returns the global {@link MetricProducerManager} which can be used to register handlers to
+   * export all the recorded metrics.
+   *
+   * @return the implementation of the {@code MetricExporter} or no-op if no implementation linked
+   *     in the binary.
+   * @since 0.17
+   */
+  public abstract MetricProducerManager getMetricProducerManager();
+
+  private static final class NoopExportComponent extends ExportComponent {
+
+    private static final MetricProducerManager METRIC_PRODUCER_MANAGER =
+        MetricProducerManager.newNoopMetricProducerManager();
+
+    @Override
+    public MetricProducerManager getMetricProducerManager() {
+      return METRIC_PRODUCER_MANAGER;
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Metric.java b/api/src/main/java/io/opencensus/metrics/export/Metric.java
new file mode 100644
index 0000000..7b93fc8
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Metric.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import io.opencensus.metrics.export.Value.ValueDistribution;
+import io.opencensus.metrics.export.Value.ValueDouble;
+import io.opencensus.metrics.export.Value.ValueLong;
+import io.opencensus.metrics.export.Value.ValueSummary;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A {@link Metric} with one or more {@link TimeSeries}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class Metric {
+
+  Metric() {}
+
+  /**
+   * Creates a {@link Metric}.
+   *
+   * @param metricDescriptor the {@link MetricDescriptor}.
+   * @param timeSeriesList the {@link TimeSeries} list for this metric.
+   * @return a {@code Metric}.
+   * @since 0.17
+   */
+  public static Metric create(MetricDescriptor metricDescriptor, List<TimeSeries> timeSeriesList) {
+    Utils.checkListElementNotNull(
+        Utils.checkNotNull(timeSeriesList, "timeSeriesList"), "timeSeries");
+    return createInternal(
+        metricDescriptor, Collections.unmodifiableList(new ArrayList<TimeSeries>(timeSeriesList)));
+  }
+
+  /**
+   * Creates a {@link Metric}.
+   *
+   * @param metricDescriptor the {@link MetricDescriptor}.
+   * @param timeSeries the single {@link TimeSeries} for this metric.
+   * @return a {@code Metric}.
+   * @since 0.17
+   */
+  public static Metric createWithOneTimeSeries(
+      MetricDescriptor metricDescriptor, TimeSeries timeSeries) {
+    return createInternal(
+        metricDescriptor, Collections.singletonList(Utils.checkNotNull(timeSeries, "timeSeries")));
+  }
+
+  /**
+   * Creates a {@link Metric}.
+   *
+   * @param metricDescriptor the {@link MetricDescriptor}.
+   * @param timeSeriesList the {@link TimeSeries} list for this metric.
+   * @return a {@code Metric}.
+   * @since 0.17
+   */
+  private static Metric createInternal(
+      MetricDescriptor metricDescriptor, List<TimeSeries> timeSeriesList) {
+    Utils.checkNotNull(metricDescriptor, "metricDescriptor");
+    checkTypeMatch(metricDescriptor.getType(), timeSeriesList);
+    return new AutoValue_Metric(metricDescriptor, timeSeriesList);
+  }
+
+  /**
+   * Returns the {@link MetricDescriptor} of this metric.
+   *
+   * @return the {@code MetricDescriptor} of this metric.
+   * @since 0.17
+   */
+  public abstract MetricDescriptor getMetricDescriptor();
+
+  /**
+   * Returns the {@link TimeSeries} list for this metric.
+   *
+   * <p>The type of the {@link TimeSeries#getPoints()} must match {@link MetricDescriptor.Type}.
+   *
+   * @return the {@code TimeSeriesList} for this metric.
+   * @since 0.17
+   */
+  public abstract List<TimeSeries> getTimeSeriesList();
+
+  private static void checkTypeMatch(MetricDescriptor.Type type, List<TimeSeries> timeSeriesList) {
+    for (TimeSeries timeSeries : timeSeriesList) {
+      for (Point point : timeSeries.getPoints()) {
+        Value value = point.getValue();
+        String valueClassName = "";
+        if (value.getClass().getSuperclass() != null) { // work around nullness check
+          // AutoValue classes should always have a super class.
+          valueClassName = value.getClass().getSuperclass().getSimpleName();
+        }
+        switch (type) {
+          case GAUGE_INT64:
+          case CUMULATIVE_INT64:
+            Utils.checkArgument(
+                value instanceof ValueLong, "Type mismatch: %s, %s.", type, valueClassName);
+            break;
+          case CUMULATIVE_DOUBLE:
+          case GAUGE_DOUBLE:
+            Utils.checkArgument(
+                value instanceof ValueDouble, "Type mismatch: %s, %s.", type, valueClassName);
+            break;
+          case GAUGE_DISTRIBUTION:
+          case CUMULATIVE_DISTRIBUTION:
+            Utils.checkArgument(
+                value instanceof ValueDistribution, "Type mismatch: %s, %s.", type, valueClassName);
+            break;
+          case SUMMARY:
+            Utils.checkArgument(
+                value instanceof ValueSummary, "Type mismatch: %s, %s.", type, valueClassName);
+        }
+      }
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/MetricDescriptor.java b/api/src/main/java/io/opencensus/metrics/export/MetricDescriptor.java
new file mode 100644
index 0000000..a4629f8
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/MetricDescriptor.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import io.opencensus.metrics.LabelKey;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link MetricDescriptor} defines a {@code Metric} type and its schema.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class MetricDescriptor {
+
+  MetricDescriptor() {}
+
+  /**
+   * Creates a {@link MetricDescriptor}.
+   *
+   * @param name name of {@code MetricDescriptor}.
+   * @param description description of {@code MetricDescriptor}.
+   * @param unit the metric unit.
+   * @param type type of {@code MetricDescriptor}.
+   * @param labelKeys the label keys associated with the {@code MetricDescriptor}.
+   * @return a {@code MetricDescriptor}.
+   * @since 0.17
+   */
+  public static MetricDescriptor create(
+      String name, String description, String unit, Type type, List<LabelKey> labelKeys) {
+    Utils.checkNotNull(labelKeys, "labelKeys");
+    Utils.checkListElementNotNull(labelKeys, "labelKey");
+    return new AutoValue_MetricDescriptor(
+        name,
+        description,
+        unit,
+        type,
+        Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+  }
+
+  /**
+   * Returns the metric descriptor name.
+   *
+   * @return the metric descriptor name.
+   * @since 0.17
+   */
+  public abstract String getName();
+
+  /**
+   * Returns the description of this metric descriptor.
+   *
+   * @return the description of this metric descriptor.
+   * @since 0.17
+   */
+  public abstract String getDescription();
+
+  /**
+   * Returns the unit of this metric descriptor.
+   *
+   * @return the unit of this metric descriptor.
+   * @since 0.17
+   */
+  public abstract String getUnit();
+
+  /**
+   * Returns the type of this metric descriptor.
+   *
+   * @return the type of this metric descriptor.
+   * @since 0.17
+   */
+  public abstract Type getType();
+
+  /**
+   * Returns the label keys associated with this metric descriptor.
+   *
+   * @return the label keys associated with this metric descriptor.
+   * @since 0.17
+   */
+  public abstract List<LabelKey> getLabelKeys();
+
+  /**
+   * The kind of metric. It describes how the data is reported.
+   *
+   * <p>A gauge is an instantaneous measurement of a value.
+   *
+   * <p>A cumulative measurement is a value accumulated over a time interval. In a time series,
+   * cumulative measurements should have the same start time and increasing end times, until an
+   * event resets the cumulative value to zero and sets a new start time for the following points.
+   *
+   * @since 0.17
+   */
+  public enum Type {
+
+    /**
+     * An instantaneous measurement of an int64 value.
+     *
+     * @since 0.17
+     */
+    GAUGE_INT64,
+
+    /**
+     * An instantaneous measurement of a double value.
+     *
+     * @since 0.17
+     */
+    GAUGE_DOUBLE,
+
+    /**
+     * An instantaneous measurement of a distribution value. The count and sum can go both up and
+     * down. Used in scenarios like a snapshot of time the current items in a queue have spent
+     * there.
+     *
+     * @since 0.17
+     */
+    GAUGE_DISTRIBUTION,
+
+    /**
+     * An cumulative measurement of an int64 value.
+     *
+     * @since 0.17
+     */
+    CUMULATIVE_INT64,
+
+    /**
+     * An cumulative measurement of a double value.
+     *
+     * @since 0.17
+     */
+    CUMULATIVE_DOUBLE,
+
+    /**
+     * An cumulative measurement of a distribution value. The count and sum can only go up, if
+     * resets then the start_time should also be reset.
+     *
+     * @since 0.17
+     */
+    CUMULATIVE_DISTRIBUTION,
+
+    /**
+     * Some frameworks implemented DISTRIBUTION as a summary of observations (usually things like
+     * request durations and response sizes). While it also provides a total count of observations
+     * and a sum of all observed values, it calculates configurable quantiles over a sliding time
+     * window.
+     *
+     * <p>This is not recommended, since it cannot be aggregated.
+     *
+     * @since 0.17
+     */
+    SUMMARY,
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/MetricProducer.java b/api/src/main/java/io/opencensus/metrics/export/MetricProducer.java
new file mode 100644
index 0000000..739a0a9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/MetricProducer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import io.opencensus.common.ExperimentalApi;
+import java.util.Collection;
+
+/**
+ * A {@link Metric} producer that can be registered for exporting using {@link
+ * MetricProducerManager}.
+ *
+ * <p>All implementation MUST be thread-safe.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public abstract class MetricProducer {
+
+  /**
+   * Returns a collection of produced {@link Metric}s to be exported.
+   *
+   * @return a collection of produced {@link Metric}s to be exported.
+   * @since 0.17
+   */
+  public abstract Collection<Metric> getMetrics();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/MetricProducerManager.java b/api/src/main/java/io/opencensus/metrics/export/MetricProducerManager.java
new file mode 100644
index 0000000..304d929
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/MetricProducerManager.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import java.util.Collections;
+import java.util.Set;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Keeps a set of {@link MetricProducer} that is used by exporters to determine the metrics that
+ * need to be exported.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@ThreadSafe
+public abstract class MetricProducerManager {
+
+  /**
+   * Adds the {@link MetricProducer} to the manager if it is not already present.
+   *
+   * @param metricProducer the {@code MetricProducer} to be added to the manager.
+   * @since 0.17
+   */
+  public abstract void add(MetricProducer metricProducer);
+
+  /**
+   * Removes the {@link MetricProducer} to the manager if it is present.
+   *
+   * @param metricProducer the {@code MetricProducer} to be removed from the manager.
+   * @since 0.17
+   */
+  public abstract void remove(MetricProducer metricProducer);
+
+  /**
+   * Returns all registered {@link MetricProducer}s that should be exported.
+   *
+   * <p>This method should be used by any metrics exporter that automatically exports data for
+   * {@code MetricProducer} registered with the {@code MetricProducerManager}.
+   *
+   * @return all registered {@code MetricProducer}s that should be exported.
+   * @since 0.17
+   */
+  public abstract Set<MetricProducer> getAllMetricProducer();
+
+  /**
+   * Returns a no-op implementation for {@link MetricProducerManager}.
+   *
+   * @return a no-op implementation for {@code MetricProducerManager}.
+   */
+  static MetricProducerManager newNoopMetricProducerManager() {
+    return new NoopMetricProducerManager();
+  }
+
+  private static final class NoopMetricProducerManager extends MetricProducerManager {
+
+    @Override
+    public void add(MetricProducer metricProducer) {
+      Utils.checkNotNull(metricProducer, "metricProducer");
+    }
+
+    @Override
+    public void remove(MetricProducer metricProducer) {
+      Utils.checkNotNull(metricProducer, "metricProducer");
+    }
+
+    @Override
+    public Set<MetricProducer> getAllMetricProducer() {
+      return Collections.emptySet();
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Point.java b/api/src/main/java/io/opencensus/metrics/export/Point.java
new file mode 100644
index 0000000..1f382f9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Point.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A timestamped measurement of a {@code TimeSeries}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@AutoValue
+@Immutable
+public abstract class Point {
+
+  Point() {}
+
+  /**
+   * Creates a {@link Point}.
+   *
+   * @param value the {@link Value} of this {@link Point}.
+   * @param timestamp the {@link Timestamp} when this {@link Point} was recorded.
+   * @return a {@code Point}.
+   * @since 0.17
+   */
+  public static Point create(Value value, Timestamp timestamp) {
+    return new AutoValue_Point(value, timestamp);
+  }
+
+  /**
+   * Returns the {@link Value}.
+   *
+   * @return the {@code Value}.
+   * @since 0.17
+   */
+  public abstract Value getValue();
+
+  /**
+   * Returns the {@link Timestamp} when this {@link Point} was recorded.
+   *
+   * @return the {@code Timestamp}.
+   * @since 0.17
+   */
+  public abstract Timestamp getTimestamp();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Summary.java b/api/src/main/java/io/opencensus/metrics/export/Summary.java
new file mode 100644
index 0000000..c82ca96
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Summary.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Implementation of the {@link Distribution} as a summary of observations.
+ *
+ * <p>This is not recommended, since it cannot be aggregated.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@AutoValue
+@Immutable
+public abstract class Summary {
+  Summary() {}
+
+  /**
+   * Creates a {@link Summary}.
+   *
+   * @param count the count of the population values.
+   * @param sum the sum of the population values.
+   * @param snapshot bucket boundaries of a histogram.
+   * @return a {@code Summary} with the given values.
+   * @since 0.17
+   */
+  public static Summary create(@Nullable Long count, @Nullable Double sum, Snapshot snapshot) {
+    checkCountAndSum(count, sum);
+    Utils.checkNotNull(snapshot, "snapshot");
+    return new AutoValue_Summary(count, sum, snapshot);
+  }
+
+  /**
+   * Returns the aggregated count. If not available returns {@code null}.
+   *
+   * @return the aggregated count.
+   * @since 0.17
+   */
+  @Nullable
+  public abstract Long getCount();
+
+  /**
+   * Returns the aggregated sum. If not available returns {@code null}.
+   *
+   * @return the aggregated sum.
+   * @since 0.17
+   */
+  @Nullable
+  public abstract Double getSum();
+
+  /**
+   * Returns the {@link Snapshot}.
+   *
+   * @return the {@code Snapshot}.
+   * @since 0.17
+   */
+  public abstract Snapshot getSnapshot();
+
+  /**
+   * Represents the summary observation of the recorded events over a sliding time window.
+   *
+   * @since 0.17
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class Snapshot {
+    /**
+     * Returns the number of values in this {@code Snapshot}. If not available returns {@code null}.
+     *
+     * @return the number of values in this {@code Snapshot}.
+     * @since 0.17
+     */
+    @Nullable
+    public abstract Long getCount();
+
+    /**
+     * Returns the sum of values in this {@code Snapshot}. If not available returns {@code null}.
+     *
+     * @return the sum of values in this {@code Snapshot}.
+     * @since 0.17
+     */
+    @Nullable
+    public abstract Double getSum();
+
+    /**
+     * Returns the list of {@code ValueAtPercentile}s in this {@code Snapshot}.
+     *
+     * @return the list of {@code ValueAtPercentile}s in this {@code Snapshot}.
+     * @since 0.17
+     */
+    public abstract List<ValueAtPercentile> getValueAtPercentiles();
+
+    /**
+     * Creates a {@link Snapshot}.
+     *
+     * @param count the number of values in this {@code Snapshot}.
+     * @param sum the number of values in this {@code Snapshot}.
+     * @param valueAtPercentiles the list of {@code ValueAtPercentile}.
+     * @return a {@code Snapshot} with the given values.
+     * @since 0.17
+     */
+    public static Snapshot create(
+        @Nullable Long count, @Nullable Double sum, List<ValueAtPercentile> valueAtPercentiles) {
+      checkCountAndSum(count, sum);
+      Utils.checkNotNull(valueAtPercentiles, "valueAtPercentiles");
+      Utils.checkListElementNotNull(valueAtPercentiles, "value in valueAtPercentiles");
+      return new AutoValue_Summary_Snapshot(
+          count,
+          sum,
+          Collections.unmodifiableList(new ArrayList<ValueAtPercentile>(valueAtPercentiles)));
+    }
+
+    /**
+     * Represents the value at a given percentile of a distribution.
+     *
+     * @since 0.17
+     */
+    @Immutable
+    @AutoValue
+    public abstract static class ValueAtPercentile {
+      /**
+       * Returns the percentile in this {@code ValueAtPercentile}.
+       *
+       * <p>Must be in the interval (0.0, 100.0].
+       *
+       * @return the percentile in this {@code ValueAtPercentile}.
+       * @since 0.17
+       */
+      public abstract double getPercentile();
+
+      /**
+       * Returns the value in this {@code ValueAtPercentile}.
+       *
+       * @return the value in this {@code ValueAtPercentile}.
+       * @since 0.17
+       */
+      public abstract double getValue();
+
+      /**
+       * Creates a {@link ValueAtPercentile}.
+       *
+       * @param percentile the percentile in this {@code ValueAtPercentile}.
+       * @param value the value in this {@code ValueAtPercentile}.
+       * @return a {@code ValueAtPercentile} with the given values.
+       * @since 0.17
+       */
+      public static ValueAtPercentile create(double percentile, double value) {
+        Utils.checkArgument(
+            0 < percentile && percentile <= 100.0,
+            "percentile must be in the interval (0.0, 100.0]");
+        Utils.checkArgument(value >= 0, "value must be non-negative");
+        return new AutoValue_Summary_Snapshot_ValueAtPercentile(percentile, value);
+      }
+    }
+  }
+
+  private static void checkCountAndSum(@Nullable Long count, @Nullable Double sum) {
+    Utils.checkArgument(count == null || count >= 0, "count must be non-negative.");
+    Utils.checkArgument(sum == null || sum >= 0, "sum must be non-negative.");
+    if (count != null && count == 0) {
+      Utils.checkArgument(sum == null || sum == 0, "sum must be 0 if count is 0.");
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/TimeSeries.java b/api/src/main/java/io/opencensus/metrics/export/TimeSeries.java
new file mode 100644
index 0000000..bfaeae9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/TimeSeries.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A collection of data points that describes the time-varying values of a {@code Metric}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class TimeSeries {
+
+  TimeSeries() {}
+
+  /**
+   * Creates a {@link TimeSeries}.
+   *
+   * @param labelValues the {@code LabelValue}s that uniquely identify this {@code TimeSeries}.
+   * @param points the data {@code Point}s of this {@code TimeSeries}.
+   * @param startTimestamp the start {@code Timestamp} of this {@code TimeSeries}. Must be non-null
+   *     for cumulative {@code Point}s.
+   * @return a {@code TimeSeries}.
+   * @since 0.17
+   */
+  public static TimeSeries create(
+      List<LabelValue> labelValues, List<Point> points, @Nullable Timestamp startTimestamp) {
+    Utils.checkNotNull(points, "points");
+    Utils.checkListElementNotNull(points, "point");
+    return createInternal(
+        labelValues, Collections.unmodifiableList(new ArrayList<Point>(points)), startTimestamp);
+  }
+
+  /**
+   * Creates a {@link TimeSeries}.
+   *
+   * @param labelValues the {@code LabelValue}s that uniquely identify this {@code TimeSeries}.
+   * @param point the single data {@code Point} of this {@code TimeSeries}.
+   * @param startTimestamp the start {@code Timestamp} of this {@code TimeSeries}. Must be non-null
+   *     for cumulative {@code Point}s.
+   * @return a {@code TimeSeries}.
+   * @since 0.17
+   */
+  public static TimeSeries createWithOnePoint(
+      List<LabelValue> labelValues, Point point, @Nullable Timestamp startTimestamp) {
+    Utils.checkNotNull(point, "point");
+    return createInternal(labelValues, Collections.singletonList(point), startTimestamp);
+  }
+
+  /**
+   * Creates a {@link TimeSeries}.
+   *
+   * @param labelValues the {@code LabelValue}s that uniquely identify this {@code TimeSeries}.
+   * @param points the data {@code Point}s of this {@code TimeSeries}.
+   * @param startTimestamp the start {@code Timestamp} of this {@code TimeSeries}. Must be non-null
+   *     for cumulative {@code Point}s.
+   * @return a {@code TimeSeries}.
+   */
+  private static TimeSeries createInternal(
+      List<LabelValue> labelValues, List<Point> points, @Nullable Timestamp startTimestamp) {
+    // Fail fast on null lists to prevent NullPointerException when copying the lists.
+    Utils.checkNotNull(labelValues, "labelValues");
+    Utils.checkListElementNotNull(labelValues, "labelValue");
+    return new AutoValue_TimeSeries(
+        Collections.unmodifiableList(new ArrayList<LabelValue>(labelValues)),
+        points,
+        startTimestamp);
+  }
+
+  /**
+   * Returns the set of {@link LabelValue}s that uniquely identify this {@link TimeSeries}.
+   *
+   * <p>Apply to all {@link Point}s.
+   *
+   * <p>The order of {@link LabelValue}s must match that of {@link LabelKey}s in the {@code
+   * MetricDescriptor}.
+   *
+   * @return the {@code LabelValue}s.
+   * @since 0.17
+   */
+  public abstract List<LabelValue> getLabelValues();
+
+  /**
+   * Returns the data {@link Point}s of this {@link TimeSeries}.
+   *
+   * @return the data {@code Point}s.
+   * @since 0.17
+   */
+  public abstract List<Point> getPoints();
+
+  /**
+   * Returns the start {@link Timestamp} of this {@link TimeSeries} if the {@link Point}s are
+   * cumulative, or {@code null} if the {@link Point}s are gauge.
+   *
+   * @return the start {@code Timestamp} or {@code null}.
+   * @since 0.17
+   */
+  @Nullable
+  public abstract Timestamp getStartTimestamp();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Value.java b/api/src/main/java/io/opencensus/metrics/export/Value.java
new file mode 100644
index 0000000..00a939c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Value.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.Function;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The actual point value for a {@link Point}.
+ *
+ * <p>Currently there are three types of {@link Value}:
+ *
+ * <ul>
+ *   <li>{@code double}
+ *   <li>{@code long}
+ *   <li>{@link Distribution}
+ * </ul>
+ *
+ * <p>Each {@link Point} contains exactly one of the three {@link Value} types.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@Immutable
+public abstract class Value {
+
+  Value() {}
+
+  /**
+   * Returns a double {@link Value}.
+   *
+   * @param value value in double.
+   * @return a double {@code Value}.
+   * @since 0.17
+   */
+  public static Value doubleValue(double value) {
+    return ValueDouble.create(value);
+  }
+
+  /**
+   * Returns a long {@link Value}.
+   *
+   * @param value value in long.
+   * @return a long {@code Value}.
+   * @since 0.17
+   */
+  public static Value longValue(long value) {
+    return ValueLong.create(value);
+  }
+
+  /**
+   * Returns a {@link Distribution} {@link Value}.
+   *
+   * @param value value in {@link Distribution}.
+   * @return a {@code Distribution} {@code Value}.
+   * @since 0.17
+   */
+  public static Value distributionValue(Distribution value) {
+    return ValueDistribution.create(value);
+  }
+
+  /**
+   * Returns a {@link Summary} {@link Value}.
+   *
+   * @param value value in {@link Summary}.
+   * @return a {@code Summary} {@code Value}.
+   * @since 0.17
+   */
+  public static Value summaryValue(Summary value) {
+    return ValueSummary.create(value);
+  }
+
+  /**
+   * Applies the given match function to the underlying data type.
+   *
+   * @since 0.17
+   */
+  public abstract <T> T match(
+      Function<? super Double, T> doubleFunction,
+      Function<? super Long, T> longFunction,
+      Function<? super Distribution, T> distributionFunction,
+      Function<? super Summary, T> summaryFunction,
+      Function<? super Value, T> defaultFunction);
+
+  /** A 64-bit double-precision floating-point {@link Value}. */
+  @AutoValue
+  @Immutable
+  abstract static class ValueDouble extends Value {
+
+    ValueDouble() {}
+
+    @Override
+    public final <T> T match(
+        Function<? super Double, T> doubleFunction,
+        Function<? super Long, T> longFunction,
+        Function<? super Distribution, T> distributionFunction,
+        Function<? super Summary, T> summaryFunction,
+        Function<? super Value, T> defaultFunction) {
+      return doubleFunction.apply(getValue());
+    }
+
+    /**
+     * Creates a {@link ValueDouble}.
+     *
+     * @param value the value in double.
+     * @return a {@code ValueDouble}.
+     */
+    static ValueDouble create(double value) {
+      return new AutoValue_Value_ValueDouble(value);
+    }
+
+    /**
+     * Returns the double value.
+     *
+     * @return the double value.
+     */
+    abstract double getValue();
+  }
+
+  /** A 64-bit integer {@link Value}. */
+  @AutoValue
+  @Immutable
+  abstract static class ValueLong extends Value {
+
+    ValueLong() {}
+
+    @Override
+    public final <T> T match(
+        Function<? super Double, T> doubleFunction,
+        Function<? super Long, T> longFunction,
+        Function<? super Distribution, T> distributionFunction,
+        Function<? super Summary, T> summaryFunction,
+        Function<? super Value, T> defaultFunction) {
+      return longFunction.apply(getValue());
+    }
+
+    /**
+     * Creates a {@link ValueLong}.
+     *
+     * @param value the value in long.
+     * @return a {@code ValueLong}.
+     */
+    static ValueLong create(long value) {
+      return new AutoValue_Value_ValueLong(value);
+    }
+
+    /**
+     * Returns the long value.
+     *
+     * @return the long value.
+     */
+    abstract long getValue();
+  }
+
+  /**
+   * {@link ValueDistribution} contains summary statistics for a population of values. It optionally
+   * contains a histogram representing the distribution of those values across a set of buckets.
+   */
+  @AutoValue
+  @Immutable
+  abstract static class ValueDistribution extends Value {
+
+    ValueDistribution() {}
+
+    @Override
+    public final <T> T match(
+        Function<? super Double, T> doubleFunction,
+        Function<? super Long, T> longFunction,
+        Function<? super Distribution, T> distributionFunction,
+        Function<? super Summary, T> summaryFunction,
+        Function<? super Value, T> defaultFunction) {
+      return distributionFunction.apply(getValue());
+    }
+
+    /**
+     * Creates a {@link ValueDistribution}.
+     *
+     * @param value the {@link Distribution} value.
+     * @return a {@code ValueDistribution}.
+     */
+    static ValueDistribution create(Distribution value) {
+      return new AutoValue_Value_ValueDistribution(value);
+    }
+
+    /**
+     * Returns the {@link Distribution} value.
+     *
+     * @return the {@code Distribution} value.
+     */
+    abstract Distribution getValue();
+  }
+
+  /**
+   * {@link ValueSummary} contains a snapshot representing values calculated over an arbitrary time
+   * window.
+   */
+  @AutoValue
+  @Immutable
+  abstract static class ValueSummary extends Value {
+
+    ValueSummary() {}
+
+    @Override
+    public final <T> T match(
+        Function<? super Double, T> doubleFunction,
+        Function<? super Long, T> longFunction,
+        Function<? super Distribution, T> distributionFunction,
+        Function<? super Summary, T> summaryFunction,
+        Function<? super Value, T> defaultFunction) {
+      return summaryFunction.apply(getValue());
+    }
+
+    /**
+     * Creates a {@link ValueSummary}.
+     *
+     * @param value the {@link Summary} value.
+     * @return a {@code ValueSummary}.
+     */
+    static ValueSummary create(Summary value) {
+      return new AutoValue_Value_ValueSummary(value);
+    }
+
+    /**
+     * Returns the {@link Summary} value.
+     *
+     * @return the {@code Summary} value.
+     */
+    abstract Summary getValue();
+  }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/package-info.java b/api/src/main/java/io/opencensus/metrics/package-info.java
new file mode 100644
index 0000000..33eadf0
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/**
+ * This package describes the Metrics data model. Metrics are a data model for what stats exporters
+ * take as input. This data model may eventually become the wire format for metrics.
+ *
+ * <p>WARNING: Currently all the public classes under this package are marked as {@link
+ * io.opencensus.common.ExperimentalApi}. The classes and APIs under {@link io.opencensus.metrics}
+ * are likely to get backwards-incompatible updates in the future. DO NOT USE except for
+ * experimental purposes.
+ *
+ * <p>Please see
+ * https://github.com/census-instrumentation/opencensus-specs/blob/master/stats/Metrics.md and
+ * https://github.com/census-instrumentation/opencensus-proto/blob/master/opencensus/proto/stats/metrics/metrics.proto
+ * for more details.
+ */
+@io.opencensus.common.ExperimentalApi
+package io.opencensus.metrics;
diff --git a/api/src/main/java/io/opencensus/stats/Aggregation.java b/api/src/main/java/io/opencensus/stats/Aggregation.java
new file mode 100644
index 0000000..9c95e84
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/Aggregation.java
@@ -0,0 +1,239 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link Aggregation} is the process of combining a certain set of {@code MeasureValue}s for a
+ * given {@code Measure} into an {@link AggregationData}.
+ *
+ * <p>{@link Aggregation} currently supports 4 types of basic aggregation:
+ *
+ * <ul>
+ *   <li>Sum
+ *   <li>Count
+ *   <li>Distribution
+ *   <li>LastValue
+ * </ul>
+ *
+ * <p>When creating a {@link View}, one {@link Aggregation} needs to be specified as how to
+ * aggregate {@code MeasureValue}s.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class Aggregation {
+
+  private Aggregation() {}
+
+  /**
+   * Applies the given match function to the underlying data type.
+   *
+   * @since 0.13
+   */
+  public abstract <T> T match(
+      Function<? super Sum, T> p0,
+      Function<? super Count, T> p1,
+      Function<? super Distribution, T> p2,
+      Function<? super LastValue, T> p3,
+      Function<? super Aggregation, T> defaultFunction);
+
+  /**
+   * Calculate sum on aggregated {@code MeasureValue}s.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class Sum extends Aggregation {
+
+    Sum() {}
+
+    private static final Sum INSTANCE = new AutoValue_Aggregation_Sum();
+
+    /**
+     * Construct a {@code Sum}.
+     *
+     * @return a new {@code Sum}.
+     * @since 0.8
+     */
+    public static Sum create() {
+      return INSTANCE;
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super Sum, T> p0,
+        Function<? super Count, T> p1,
+        Function<? super Distribution, T> p2,
+        Function<? super LastValue, T> p3,
+        Function<? super Aggregation, T> defaultFunction) {
+      return p0.apply(this);
+    }
+  }
+
+  /**
+   * Calculate count on aggregated {@code MeasureValue}s.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class Count extends Aggregation {
+
+    Count() {}
+
+    private static final Count INSTANCE = new AutoValue_Aggregation_Count();
+
+    /**
+     * Construct a {@code Count}.
+     *
+     * @return a new {@code Count}.
+     * @since 0.8
+     */
+    public static Count create() {
+      return INSTANCE;
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super Sum, T> p0,
+        Function<? super Count, T> p1,
+        Function<? super Distribution, T> p2,
+        Function<? super LastValue, T> p3,
+        Function<? super Aggregation, T> defaultFunction) {
+      return p1.apply(this);
+    }
+  }
+
+  /**
+   * Calculate mean on aggregated {@code MeasureValue}s.
+   *
+   * @since 0.8
+   * @deprecated since 0.13, use {@link Distribution} instead.
+   */
+  @Immutable
+  @AutoValue
+  @Deprecated
+  @AutoValue.CopyAnnotations
+  public abstract static class Mean extends Aggregation {
+
+    Mean() {}
+
+    private static final Mean INSTANCE = new AutoValue_Aggregation_Mean();
+
+    /**
+     * Construct a {@code Mean}.
+     *
+     * @return a new {@code Mean}.
+     * @since 0.8
+     */
+    public static Mean create() {
+      return INSTANCE;
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super Sum, T> p0,
+        Function<? super Count, T> p1,
+        Function<? super Distribution, T> p2,
+        Function<? super LastValue, T> p3,
+        Function<? super Aggregation, T> defaultFunction) {
+      return defaultFunction.apply(this);
+    }
+  }
+
+  /**
+   * Calculate distribution stats on aggregated {@code MeasureValue}s. Distribution includes mean,
+   * count, histogram, min, max and sum of squared deviations.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class Distribution extends Aggregation {
+
+    Distribution() {}
+
+    /**
+     * Construct a {@code Distribution}.
+     *
+     * @return a new {@code Distribution}.
+     * @since 0.8
+     */
+    public static Distribution create(BucketBoundaries bucketBoundaries) {
+      Utils.checkNotNull(bucketBoundaries, "bucketBoundaries");
+      return new AutoValue_Aggregation_Distribution(bucketBoundaries);
+    }
+
+    /**
+     * Returns the {@code Distribution}'s bucket boundaries.
+     *
+     * @return the {@code Distribution}'s bucket boundaries.
+     * @since 0.8
+     */
+    public abstract BucketBoundaries getBucketBoundaries();
+
+    @Override
+    public final <T> T match(
+        Function<? super Sum, T> p0,
+        Function<? super Count, T> p1,
+        Function<? super Distribution, T> p2,
+        Function<? super LastValue, T> p3,
+        Function<? super Aggregation, T> defaultFunction) {
+      return p2.apply(this);
+    }
+  }
+
+  /**
+   * Calculate the last value of aggregated {@code MeasureValue}s.
+   *
+   * @since 0.13
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class LastValue extends Aggregation {
+
+    LastValue() {}
+
+    private static final LastValue INSTANCE = new AutoValue_Aggregation_LastValue();
+
+    /**
+     * Construct a {@code LastValue}.
+     *
+     * @return a new {@code LastValue}.
+     * @since 0.13
+     */
+    public static LastValue create() {
+      return INSTANCE;
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super Sum, T> p0,
+        Function<? super Count, T> p1,
+        Function<? super Distribution, T> p2,
+        Function<? super LastValue, T> p3,
+        Function<? super Aggregation, T> defaultFunction) {
+      return p3.apply(this);
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/stats/AggregationData.java b/api/src/main/java/io/opencensus/stats/AggregationData.java
new file mode 100644
index 0000000..c6e12b6
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/AggregationData.java
@@ -0,0 +1,555 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link AggregationData} is the result of applying a given {@link Aggregation} to a set of {@code
+ * MeasureValue}s.
+ *
+ * <p>{@link AggregationData} currently supports 6 types of basic aggregation values:
+ *
+ * <ul>
+ *   <li>SumDataDouble
+ *   <li>SumDataLong
+ *   <li>CountData
+ *   <li>DistributionData
+ *   <li>LastValueDataDouble
+ *   <li>LastValueDataLong
+ * </ul>
+ *
+ * <p>{@link ViewData} will contain one {@link AggregationData}, corresponding to its {@link
+ * Aggregation} definition in {@link View}.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class AggregationData {
+
+  private AggregationData() {}
+
+  /**
+   * Applies the given match function to the underlying data type.
+   *
+   * @since 0.13
+   */
+  public abstract <T> T match(
+      Function<? super SumDataDouble, T> p0,
+      Function<? super SumDataLong, T> p1,
+      Function<? super CountData, T> p2,
+      Function<? super DistributionData, T> p3,
+      Function<? super LastValueDataDouble, T> p4,
+      Function<? super LastValueDataLong, T> p5,
+      Function<? super AggregationData, T> defaultFunction);
+
+  /**
+   * The sum value of aggregated {@code MeasureValueDouble}s.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class SumDataDouble extends AggregationData {
+
+    SumDataDouble() {}
+
+    /**
+     * Creates a {@code SumDataDouble}.
+     *
+     * @param sum the aggregated sum.
+     * @return a {@code SumDataDouble}.
+     * @since 0.8
+     */
+    public static SumDataDouble create(double sum) {
+      return new AutoValue_AggregationData_SumDataDouble(sum);
+    }
+
+    /**
+     * Returns the aggregated sum.
+     *
+     * @return the aggregated sum.
+     * @since 0.8
+     */
+    public abstract double getSum();
+
+    @Override
+    public final <T> T match(
+        Function<? super SumDataDouble, T> p0,
+        Function<? super SumDataLong, T> p1,
+        Function<? super CountData, T> p2,
+        Function<? super DistributionData, T> p3,
+        Function<? super LastValueDataDouble, T> p4,
+        Function<? super LastValueDataLong, T> p5,
+        Function<? super AggregationData, T> defaultFunction) {
+      return p0.apply(this);
+    }
+  }
+
+  /**
+   * The sum value of aggregated {@code MeasureValueLong}s.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class SumDataLong extends AggregationData {
+
+    SumDataLong() {}
+
+    /**
+     * Creates a {@code SumDataLong}.
+     *
+     * @param sum the aggregated sum.
+     * @return a {@code SumDataLong}.
+     * @since 0.8
+     */
+    public static SumDataLong create(long sum) {
+      return new AutoValue_AggregationData_SumDataLong(sum);
+    }
+
+    /**
+     * Returns the aggregated sum.
+     *
+     * @return the aggregated sum.
+     * @since 0.8
+     */
+    public abstract long getSum();
+
+    @Override
+    public final <T> T match(
+        Function<? super SumDataDouble, T> p0,
+        Function<? super SumDataLong, T> p1,
+        Function<? super CountData, T> p2,
+        Function<? super DistributionData, T> p3,
+        Function<? super LastValueDataDouble, T> p4,
+        Function<? super LastValueDataLong, T> p5,
+        Function<? super AggregationData, T> defaultFunction) {
+      return p1.apply(this);
+    }
+  }
+
+  /**
+   * The count value of aggregated {@code MeasureValue}s.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class CountData extends AggregationData {
+
+    CountData() {}
+
+    /**
+     * Creates a {@code CountData}.
+     *
+     * @param count the aggregated count.
+     * @return a {@code CountData}.
+     * @since 0.8
+     */
+    public static CountData create(long count) {
+      return new AutoValue_AggregationData_CountData(count);
+    }
+
+    /**
+     * Returns the aggregated count.
+     *
+     * @return the aggregated count.
+     * @since 0.8
+     */
+    public abstract long getCount();
+
+    @Override
+    public final <T> T match(
+        Function<? super SumDataDouble, T> p0,
+        Function<? super SumDataLong, T> p1,
+        Function<? super CountData, T> p2,
+        Function<? super DistributionData, T> p3,
+        Function<? super LastValueDataDouble, T> p4,
+        Function<? super LastValueDataLong, T> p5,
+        Function<? super AggregationData, T> defaultFunction) {
+      return p2.apply(this);
+    }
+  }
+
+  /**
+   * The mean value of aggregated {@code MeasureValue}s.
+   *
+   * @since 0.8
+   * @deprecated since 0.13, use {@link DistributionData} instead.
+   */
+  @Immutable
+  @AutoValue
+  @Deprecated
+  @AutoValue.CopyAnnotations
+  public abstract static class MeanData extends AggregationData {
+
+    MeanData() {}
+
+    /**
+     * Creates a {@code MeanData}.
+     *
+     * @param mean the aggregated mean.
+     * @param count the aggregated count.
+     * @return a {@code MeanData}.
+     * @since 0.8
+     */
+    public static MeanData create(double mean, long count) {
+      return new AutoValue_AggregationData_MeanData(mean, count);
+    }
+
+    /**
+     * Returns the aggregated mean.
+     *
+     * @return the aggregated mean.
+     * @since 0.8
+     */
+    public abstract double getMean();
+
+    /**
+     * Returns the aggregated count.
+     *
+     * @return the aggregated count.
+     * @since 0.8
+     */
+    public abstract long getCount();
+
+    @Override
+    public final <T> T match(
+        Function<? super SumDataDouble, T> p0,
+        Function<? super SumDataLong, T> p1,
+        Function<? super CountData, T> p2,
+        Function<? super DistributionData, T> p3,
+        Function<? super LastValueDataDouble, T> p4,
+        Function<? super LastValueDataLong, T> p5,
+        Function<? super AggregationData, T> defaultFunction) {
+      return defaultFunction.apply(this);
+    }
+  }
+
+  /**
+   * The distribution stats of aggregated {@code MeasureValue}s. Distribution stats include mean,
+   * count, histogram, min, max and sum of squared deviations.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class DistributionData extends AggregationData {
+
+    DistributionData() {}
+
+    /**
+     * Creates a {@code DistributionData}.
+     *
+     * @param mean mean value.
+     * @param count count value.
+     * @param min min value.
+     * @param max max value.
+     * @param sumOfSquaredDeviations sum of squared deviations.
+     * @param bucketCounts histogram bucket counts.
+     * @param exemplars the exemplars associated with histogram buckets.
+     * @return a {@code DistributionData}.
+     * @since 0.16
+     */
+    public static DistributionData create(
+        double mean,
+        long count,
+        double min,
+        double max,
+        double sumOfSquaredDeviations,
+        List<Long> bucketCounts,
+        List<Exemplar> exemplars) {
+      if (min != Double.POSITIVE_INFINITY || max != Double.NEGATIVE_INFINITY) {
+        Utils.checkArgument(min <= max, "max should be greater or equal to min.");
+      }
+
+      Utils.checkNotNull(bucketCounts, "bucketCounts");
+      List<Long> bucketCountsCopy = Collections.unmodifiableList(new ArrayList<Long>(bucketCounts));
+      for (Long bucket : bucketCountsCopy) {
+        Utils.checkNotNull(bucket, "bucket");
+      }
+
+      Utils.checkNotNull(exemplars, "exemplar list should not be null.");
+      for (Exemplar exemplar : exemplars) {
+        Utils.checkNotNull(exemplar, "exemplar");
+      }
+
+      return new AutoValue_AggregationData_DistributionData(
+          mean,
+          count,
+          min,
+          max,
+          sumOfSquaredDeviations,
+          bucketCountsCopy,
+          Collections.<Exemplar>unmodifiableList(new ArrayList<Exemplar>(exemplars)));
+    }
+
+    /**
+     * Creates a {@code DistributionData}.
+     *
+     * @param mean mean value.
+     * @param count count value.
+     * @param min min value.
+     * @param max max value.
+     * @param sumOfSquaredDeviations sum of squared deviations.
+     * @param bucketCounts histogram bucket counts.
+     * @return a {@code DistributionData}.
+     * @since 0.8
+     */
+    public static DistributionData create(
+        double mean,
+        long count,
+        double min,
+        double max,
+        double sumOfSquaredDeviations,
+        List<Long> bucketCounts) {
+      return create(
+          mean,
+          count,
+          min,
+          max,
+          sumOfSquaredDeviations,
+          bucketCounts,
+          Collections.<Exemplar>emptyList());
+    }
+
+    /**
+     * Returns the aggregated mean.
+     *
+     * @return the aggregated mean.
+     * @since 0.8
+     */
+    public abstract double getMean();
+
+    /**
+     * Returns the aggregated count.
+     *
+     * @return the aggregated count.
+     * @since 0.8
+     */
+    public abstract long getCount();
+
+    /**
+     * Returns the minimum of the population values.
+     *
+     * @return the minimum of the population values.
+     * @since 0.8
+     */
+    public abstract double getMin();
+
+    /**
+     * Returns the maximum of the population values.
+     *
+     * @return the maximum of the population values.
+     * @since 0.8
+     */
+    public abstract double getMax();
+
+    /**
+     * Returns the aggregated sum of squared deviations.
+     *
+     * @return the aggregated sum of squared deviations.
+     * @since 0.8
+     */
+    public abstract double getSumOfSquaredDeviations();
+
+    /**
+     * Returns the aggregated bucket counts. The returned list is immutable, trying to update it
+     * will throw an {@code UnsupportedOperationException}.
+     *
+     * @return the aggregated bucket counts.
+     * @since 0.8
+     */
+    public abstract List<Long> getBucketCounts();
+
+    /**
+     * Returns the {@link Exemplar}s associated with histogram buckets.
+     *
+     * @return the {@code Exemplar}s associated with histogram buckets.
+     * @since 0.16
+     */
+    public abstract List<Exemplar> getExemplars();
+
+    @Override
+    public final <T> T match(
+        Function<? super SumDataDouble, T> p0,
+        Function<? super SumDataLong, T> p1,
+        Function<? super CountData, T> p2,
+        Function<? super DistributionData, T> p3,
+        Function<? super LastValueDataDouble, T> p4,
+        Function<? super LastValueDataLong, T> p5,
+        Function<? super AggregationData, T> defaultFunction) {
+      return p3.apply(this);
+    }
+
+    /**
+     * An example point that may be used to annotate aggregated distribution values, associated with
+     * a histogram bucket.
+     *
+     * @since 0.16
+     */
+    @Immutable
+    @AutoValue
+    public abstract static class Exemplar {
+
+      Exemplar() {}
+
+      /**
+       * Returns value of the {@link Exemplar} point.
+       *
+       * @return value of the {@code Exemplar} point.
+       * @since 0.16
+       */
+      public abstract double getValue();
+
+      /**
+       * Returns the time that this {@link Exemplar}'s value was recorded.
+       *
+       * @return the time that this {@code Exemplar}'s value was recorded.
+       * @since 0.16
+       */
+      public abstract Timestamp getTimestamp();
+
+      /**
+       * Returns the contextual information about the example value, represented as a string map.
+       *
+       * @return the contextual information about the example value.
+       * @since 0.16
+       */
+      public abstract Map<String, String> getAttachments();
+
+      /**
+       * Creates an {@link Exemplar}.
+       *
+       * @param value value of the {@link Exemplar} point.
+       * @param timestamp the time that this {@code Exemplar}'s value was recorded.
+       * @param attachments the contextual information about the example value.
+       * @return an {@code Exemplar}.
+       * @since 0.16
+       */
+      public static Exemplar create(
+          double value, Timestamp timestamp, Map<String, String> attachments) {
+        Utils.checkNotNull(attachments, "attachments");
+        Map<String, String> attachmentsCopy =
+            Collections.unmodifiableMap(new HashMap<String, String>(attachments));
+        for (Entry<String, String> entry : attachmentsCopy.entrySet()) {
+          Utils.checkNotNull(entry.getKey(), "key of attachments");
+          Utils.checkNotNull(entry.getValue(), "value of attachments");
+        }
+        return new AutoValue_AggregationData_DistributionData_Exemplar(
+            value, timestamp, attachmentsCopy);
+      }
+    }
+  }
+
+  /**
+   * The last value of aggregated {@code MeasureValueDouble}s.
+   *
+   * @since 0.13
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class LastValueDataDouble extends AggregationData {
+
+    LastValueDataDouble() {}
+
+    /**
+     * Creates a {@code LastValueDataDouble}.
+     *
+     * @param lastValue the last value.
+     * @return a {@code LastValueDataDouble}.
+     * @since 0.13
+     */
+    public static LastValueDataDouble create(double lastValue) {
+      return new AutoValue_AggregationData_LastValueDataDouble(lastValue);
+    }
+
+    /**
+     * Returns the last value.
+     *
+     * @return the last value.
+     * @since 0.13
+     */
+    public abstract double getLastValue();
+
+    @Override
+    public final <T> T match(
+        Function<? super SumDataDouble, T> p0,
+        Function<? super SumDataLong, T> p1,
+        Function<? super CountData, T> p2,
+        Function<? super DistributionData, T> p3,
+        Function<? super LastValueDataDouble, T> p4,
+        Function<? super LastValueDataLong, T> p5,
+        Function<? super AggregationData, T> defaultFunction) {
+      return p4.apply(this);
+    }
+  }
+
+  /**
+   * The last value of aggregated {@code MeasureValueLong}s.
+   *
+   * @since 0.13
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class LastValueDataLong extends AggregationData {
+
+    LastValueDataLong() {}
+
+    /**
+     * Creates a {@code LastValueDataLong}.
+     *
+     * @param lastValue the last value.
+     * @return a {@code LastValueDataLong}.
+     * @since 0.13
+     */
+    public static LastValueDataLong create(long lastValue) {
+      return new AutoValue_AggregationData_LastValueDataLong(lastValue);
+    }
+
+    /**
+     * Returns the last value.
+     *
+     * @return the last value.
+     * @since 0.13
+     */
+    public abstract long getLastValue();
+
+    @Override
+    public final <T> T match(
+        Function<? super SumDataDouble, T> p0,
+        Function<? super SumDataLong, T> p1,
+        Function<? super CountData, T> p2,
+        Function<? super DistributionData, T> p3,
+        Function<? super LastValueDataDouble, T> p4,
+        Function<? super LastValueDataLong, T> p5,
+        Function<? super AggregationData, T> defaultFunction) {
+      return p5.apply(this);
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/stats/BucketBoundaries.java b/api/src/main/java/io/opencensus/stats/BucketBoundaries.java
new file mode 100644
index 0000000..61e21e6
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/BucketBoundaries.java
@@ -0,0 +1,66 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The bucket boundaries for a histogram.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+public abstract class BucketBoundaries {
+
+  /**
+   * Returns a {@code BucketBoundaries} with the given buckets.
+   *
+   * @param bucketBoundaries the boundaries for the buckets in the underlying histogram.
+   * @return a new {@code BucketBoundaries} with the specified boundaries.
+   * @throws NullPointerException if {@code bucketBoundaries} is null.
+   * @throws IllegalArgumentException if {@code bucketBoundaries} is not sorted.
+   * @since 0.8
+   */
+  public static final BucketBoundaries create(List<Double> bucketBoundaries) {
+    Utils.checkNotNull(bucketBoundaries, "bucketBoundaries");
+    List<Double> bucketBoundariesCopy = new ArrayList<Double>(bucketBoundaries); // Deep copy.
+    // Check if sorted.
+    if (bucketBoundariesCopy.size() > 1) {
+      double lower = bucketBoundariesCopy.get(0);
+      for (int i = 1; i < bucketBoundariesCopy.size(); i++) {
+        double next = bucketBoundariesCopy.get(i);
+        Utils.checkArgument(lower < next, "Bucket boundaries not sorted.");
+        lower = next;
+      }
+    }
+    return new AutoValue_BucketBoundaries(Collections.unmodifiableList(bucketBoundariesCopy));
+  }
+
+  /**
+   * Returns a list of histogram bucket boundaries.
+   *
+   * @return a list of histogram bucket boundaries.
+   * @since 0.8
+   */
+  public abstract List<Double> getBoundaries();
+}
diff --git a/api/src/main/java/io/opencensus/stats/Measure.java b/api/src/main/java/io/opencensus/stats/Measure.java
new file mode 100644
index 0000000..2de7fd7
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/Measure.java
@@ -0,0 +1,177 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.StringUtils;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The definition of the {@link Measurement} that is taken by OpenCensus library.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class Measure {
+  @DefaultVisibilityForTesting static final int NAME_MAX_LENGTH = 255;
+  private static final String ERROR_MESSAGE_INVALID_NAME =
+      "Name should be a ASCII string with a length no greater than "
+          + NAME_MAX_LENGTH
+          + " characters.";
+
+  /**
+   * Applies the given match function to the underlying data type.
+   *
+   * @since 0.8
+   */
+  public abstract <T> T match(
+      Function<? super MeasureDouble, T> p0,
+      Function<? super MeasureLong, T> p1,
+      Function<? super Measure, T> defaultFunction);
+
+  /**
+   * Name of measure, as a {@code String}. Should be a ASCII string with a length no greater than
+   * 255 characters.
+   *
+   * <p>Suggested format for name: {@code <web_host>/<path>}.
+   *
+   * @since 0.8
+   */
+  public abstract String getName();
+
+  /**
+   * Detailed description of the measure, used in documentation.
+   *
+   * @since 0.8
+   */
+  public abstract String getDescription();
+
+  /**
+   * The units in which {@link Measure} values are measured.
+   *
+   * <p>The suggested grammar for a unit is as follows:
+   *
+   * <ul>
+   *   <li>Expression = Component { "." Component } {"/" Component };
+   *   <li>Component = [ PREFIX ] UNIT [ Annotation ] | Annotation | "1";
+   *   <li>Annotation = "{" NAME "}" ;
+   * </ul>
+   *
+   * <p>For example, string “MBy{transmitted}/ms” stands for megabytes per milliseconds, and the
+   * annotation transmitted inside {} is just a comment of the unit.
+   *
+   * @since 0.8
+   */
+  // TODO(songya): determine whether we want to check the grammar on string unit.
+  public abstract String getUnit();
+
+  // Prevents this class from being subclassed anywhere else.
+  private Measure() {}
+
+  /**
+   * {@link Measure} with {@code Double} typed values.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class MeasureDouble extends Measure {
+
+    MeasureDouble() {}
+
+    /**
+     * Constructs a new {@link MeasureDouble}.
+     *
+     * @param name name of {@code Measure}. Suggested format: {@code <web_host>/<path>}.
+     * @param description description of {@code Measure}.
+     * @param unit unit of {@code Measure}.
+     * @return a {@code MeasureDouble}.
+     * @since 0.8
+     */
+    public static MeasureDouble create(String name, String description, String unit) {
+      Utils.checkArgument(
+          StringUtils.isPrintableString(name) && name.length() <= NAME_MAX_LENGTH,
+          ERROR_MESSAGE_INVALID_NAME);
+      return new AutoValue_Measure_MeasureDouble(name, description, unit);
+    }
+
+    @Override
+    public <T> T match(
+        Function<? super MeasureDouble, T> p0,
+        Function<? super MeasureLong, T> p1,
+        Function<? super Measure, T> defaultFunction) {
+      return p0.apply(this);
+    }
+
+    @Override
+    public abstract String getName();
+
+    @Override
+    public abstract String getDescription();
+
+    @Override
+    public abstract String getUnit();
+  }
+
+  /**
+   * {@link Measure} with {@code Long} typed values.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class MeasureLong extends Measure {
+
+    MeasureLong() {}
+
+    /**
+     * Constructs a new {@link MeasureLong}.
+     *
+     * @param name name of {@code Measure}. Suggested format: {@code <web_host>/<path>}.
+     * @param description description of {@code Measure}.
+     * @param unit unit of {@code Measure}.
+     * @return a {@code MeasureLong}.
+     * @since 0.8
+     */
+    public static MeasureLong create(String name, String description, String unit) {
+      Utils.checkArgument(
+          StringUtils.isPrintableString(name) && name.length() <= NAME_MAX_LENGTH,
+          ERROR_MESSAGE_INVALID_NAME);
+      return new AutoValue_Measure_MeasureLong(name, description, unit);
+    }
+
+    @Override
+    public <T> T match(
+        Function<? super MeasureDouble, T> p0,
+        Function<? super MeasureLong, T> p1,
+        Function<? super Measure, T> defaultFunction) {
+      return p1.apply(this);
+    }
+
+    @Override
+    public abstract String getName();
+
+    @Override
+    public abstract String getDescription();
+
+    @Override
+    public abstract String getUnit();
+  }
+}
diff --git a/api/src/main/java/io/opencensus/stats/MeasureMap.java b/api/src/main/java/io/opencensus/stats/MeasureMap.java
new file mode 100644
index 0000000..beb84f0
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/MeasureMap.java
@@ -0,0 +1,92 @@
+/*
+ * 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.stats;
+
+import io.opencensus.internal.Utils;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagContext;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * A map from {@link Measure}s to measured values to be recorded at the same time.
+ *
+ * @since 0.8
+ */
+@NotThreadSafe
+public abstract class MeasureMap {
+
+  /**
+   * Associates the {@link MeasureDouble} with the given value. Subsequent updates to the same
+   * {@link MeasureDouble} will overwrite the previous value.
+   *
+   * @param measure the {@link MeasureDouble}
+   * @param value the value to be associated with {@code measure}
+   * @return this
+   * @since 0.8
+   */
+  public abstract MeasureMap put(MeasureDouble measure, double value);
+
+  /**
+   * Associates the {@link MeasureLong} with the given value. Subsequent updates to the same {@link
+   * MeasureLong} will overwrite the previous value.
+   *
+   * @param measure the {@link MeasureLong}
+   * @param value the value to be associated with {@code measure}
+   * @return this
+   * @since 0.8
+   */
+  public abstract MeasureMap put(MeasureLong measure, long value);
+
+  /**
+   * Associate the contextual information of an {@code Exemplar} to this {@link MeasureMap}.
+   * Contextual information is represented as {@code String} key-value pairs.
+   *
+   * <p>If this method is called multiple times with the same key, only the last value will be kept.
+   *
+   * @param key the key of contextual information of an {@code Exemplar}.
+   * @param value the string representation of contextual information of an {@code Exemplar}.
+   * @return this
+   * @since 0.16
+   */
+  // TODO(songya): make this method abstract in the 0.17 release.
+  public MeasureMap putAttachment(String key, String value) {
+    // Provides a default no-op implementation to avoid breaking other existing sub-classes.
+    Utils.checkNotNull(key, "key");
+    Utils.checkNotNull(value, "value");
+    return this;
+  }
+
+  /**
+   * Records all of the measures at the same time, with the current {@link TagContext}.
+   *
+   * <p>This method records all of the stats in the {@code MeasureMap} every time it is called.
+   *
+   * @since 0.8
+   */
+  public abstract void record();
+
+  /**
+   * Records all of the measures at the same time, with an explicit {@link TagContext}.
+   *
+   * <p>This method records all of the stats in the {@code MeasureMap} every time it is called.
+   *
+   * @param tags the tags associated with the measurements.
+   * @since 0.8
+   */
+  public abstract void record(TagContext tags);
+}
diff --git a/api/src/main/java/io/opencensus/stats/Measurement.java b/api/src/main/java/io/opencensus/stats/Measurement.java
new file mode 100644
index 0000000..647a667
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/Measurement.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016-17, 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Immutable representation of a Measurement.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class Measurement {
+
+  /**
+   * Applies the given match function to the underlying data type.
+   *
+   * @since 0.8
+   */
+  public abstract <T> T match(
+      Function<? super MeasurementDouble, T> p0,
+      Function<? super MeasurementLong, T> p1,
+      Function<? super Measurement, T> defaultFunction);
+
+  /**
+   * Extracts the measured {@link Measure}.
+   *
+   * @since 0.8
+   */
+  public abstract Measure getMeasure();
+
+  // Prevents this class from being subclassed anywhere else.
+  private Measurement() {}
+
+  /**
+   * {@code Double} typed {@link Measurement}.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class MeasurementDouble extends Measurement {
+    MeasurementDouble() {}
+
+    /**
+     * Constructs a new {@link MeasurementDouble}.
+     *
+     * @since 0.8
+     */
+    public static MeasurementDouble create(MeasureDouble measure, double value) {
+      return new AutoValue_Measurement_MeasurementDouble(measure, value);
+    }
+
+    @Override
+    public abstract MeasureDouble getMeasure();
+
+    /**
+     * Returns the value for the measure.
+     *
+     * @return the value for the measure.
+     * @since 0.8
+     */
+    public abstract double getValue();
+
+    @Override
+    public <T> T match(
+        Function<? super MeasurementDouble, T> p0,
+        Function<? super MeasurementLong, T> p1,
+        Function<? super Measurement, T> defaultFunction) {
+      return p0.apply(this);
+    }
+  }
+
+  /**
+   * {@code Long} typed {@link Measurement}.
+   *
+   * @since 0.8
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class MeasurementLong extends Measurement {
+    MeasurementLong() {}
+
+    /**
+     * Constructs a new {@link MeasurementLong}.
+     *
+     * @since 0.8
+     */
+    public static MeasurementLong create(MeasureLong measure, long value) {
+      return new AutoValue_Measurement_MeasurementLong(measure, value);
+    }
+
+    @Override
+    public abstract MeasureLong getMeasure();
+
+    /**
+     * Returns the value for the measure.
+     *
+     * @return the value for the measure.
+     * @since 0.8
+     */
+    public abstract long getValue();
+
+    @Override
+    public <T> T match(
+        Function<? super MeasurementDouble, T> p0,
+        Function<? super MeasurementLong, T> p1,
+        Function<? super Measurement, T> defaultFunction) {
+      return p1.apply(this);
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/stats/NoopStats.java b/api/src/main/java/io/opencensus/stats/NoopStats.java
new file mode 100644
index 0000000..e7e94a3
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/NoopStats.java
@@ -0,0 +1,221 @@
+/*
+ * 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.stats;
+
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagValue;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** No-op implementations of stats classes. */
+final class NoopStats {
+
+  private NoopStats() {}
+
+  /**
+   * Returns a {@code StatsComponent} that has a no-op implementation for {@link StatsRecorder}.
+   *
+   * @return a {@code StatsComponent} that has a no-op implementation for {@code StatsRecorder}.
+   */
+  static StatsComponent newNoopStatsComponent() {
+    return new NoopStatsComponent();
+  }
+
+  /**
+   * Returns a {@code StatsRecorder} that does not record any data.
+   *
+   * @return a {@code StatsRecorder} that does not record any data.
+   */
+  static StatsRecorder getNoopStatsRecorder() {
+    return NoopStatsRecorder.INSTANCE;
+  }
+
+  /**
+   * Returns a {@code MeasureMap} that ignores all calls to {@link MeasureMap#put}.
+   *
+   * @return a {@code MeasureMap} that ignores all calls to {@code MeasureMap#put}.
+   */
+  static MeasureMap getNoopMeasureMap() {
+    return NoopMeasureMap.INSTANCE;
+  }
+
+  /**
+   * Returns a {@code ViewManager} that maintains a map of views, but always returns empty {@link
+   * ViewData}s.
+   *
+   * @return a {@code ViewManager} that maintains a map of views, but always returns empty {@code
+   *     ViewData}s.
+   */
+  static ViewManager newNoopViewManager() {
+    return new NoopViewManager();
+  }
+
+  @ThreadSafe
+  private static final class NoopStatsComponent extends StatsComponent {
+    private final ViewManager viewManager = newNoopViewManager();
+    private volatile boolean isRead;
+
+    @Override
+    public ViewManager getViewManager() {
+      return viewManager;
+    }
+
+    @Override
+    public StatsRecorder getStatsRecorder() {
+      return getNoopStatsRecorder();
+    }
+
+    @Override
+    public StatsCollectionState getState() {
+      isRead = true;
+      return StatsCollectionState.DISABLED;
+    }
+
+    @Override
+    @Deprecated
+    public void setState(StatsCollectionState state) {
+      Utils.checkNotNull(state, "state");
+      Utils.checkState(!isRead, "State was already read, cannot set state.");
+    }
+  }
+
+  @Immutable
+  private static final class NoopStatsRecorder extends StatsRecorder {
+    static final StatsRecorder INSTANCE = new NoopStatsRecorder();
+
+    @Override
+    public MeasureMap newMeasureMap() {
+      return getNoopMeasureMap();
+    }
+  }
+
+  @Immutable
+  private static final class NoopMeasureMap extends MeasureMap {
+    static final MeasureMap INSTANCE = new NoopMeasureMap();
+
+    @Override
+    public MeasureMap put(MeasureDouble measure, double value) {
+      return this;
+    }
+
+    @Override
+    public MeasureMap put(MeasureLong measure, long value) {
+      return this;
+    }
+
+    @Override
+    public void record() {}
+
+    @Override
+    public void record(TagContext tags) {
+      Utils.checkNotNull(tags, "tags");
+    }
+  }
+
+  @ThreadSafe
+  private static final class NoopViewManager extends ViewManager {
+    private static final Timestamp ZERO_TIMESTAMP = Timestamp.create(0, 0);
+
+    @GuardedBy("registeredViews")
+    private final Map<View.Name, View> registeredViews = new HashMap<View.Name, View>();
+
+    // Cached set of exported views. It must be set to null whenever a view is registered or
+    // unregistered.
+    @javax.annotation.Nullable private volatile Set<View> exportedViews;
+
+    @Override
+    public void registerView(View newView) {
+      Utils.checkNotNull(newView, "newView");
+      synchronized (registeredViews) {
+        exportedViews = null;
+        View existing = registeredViews.get(newView.getName());
+        Utils.checkArgument(
+            existing == null || newView.equals(existing),
+            "A different view with the same name already exists.");
+        if (existing == null) {
+          registeredViews.put(newView.getName(), newView);
+        }
+      }
+    }
+
+    @Override
+    @javax.annotation.Nullable
+    @SuppressWarnings("deprecation")
+    public ViewData getView(View.Name name) {
+      Utils.checkNotNull(name, "name");
+      synchronized (registeredViews) {
+        View view = registeredViews.get(name);
+        if (view == null) {
+          return null;
+        } else {
+          return ViewData.create(
+              view,
+              Collections.<List</*@Nullable*/ TagValue>, AggregationData>emptyMap(),
+              view.getWindow()
+                  .match(
+                      Functions.<ViewData.AggregationWindowData>returnConstant(
+                          ViewData.AggregationWindowData.CumulativeData.create(
+                              ZERO_TIMESTAMP, ZERO_TIMESTAMP)),
+                      Functions.<ViewData.AggregationWindowData>returnConstant(
+                          ViewData.AggregationWindowData.IntervalData.create(ZERO_TIMESTAMP)),
+                      Functions.<ViewData.AggregationWindowData>throwAssertionError()));
+        }
+      }
+    }
+
+    @Override
+    public Set<View> getAllExportedViews() {
+      Set<View> views = exportedViews;
+      if (views == null) {
+        synchronized (registeredViews) {
+          exportedViews = views = filterExportedViews(registeredViews.values());
+        }
+      }
+      return views;
+    }
+
+    // Returns the subset of the given views that should be exported
+    @SuppressWarnings("deprecation")
+    private static Set<View> filterExportedViews(Collection<View> allViews) {
+      Set<View> views = new HashSet<View>();
+      for (View view : allViews) {
+        if (view.getWindow() instanceof View.AggregationWindow.Interval) {
+          continue;
+        }
+        views.add(view);
+      }
+      return Collections.unmodifiableSet(views);
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/stats/Stats.java b/api/src/main/java/io/opencensus/stats/Stats.java
new file mode 100644
index 0000000..8393f63
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/Stats.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2016-17, 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.stats;
+
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Provider;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * Class for accessing the default {@link StatsComponent}.
+ *
+ * @since 0.8
+ */
+public final class Stats {
+  private static final Logger logger = Logger.getLogger(Stats.class.getName());
+
+  private static final StatsComponent statsComponent =
+      loadStatsComponent(StatsComponent.class.getClassLoader());
+
+  /**
+   * Returns the default {@link StatsRecorder}.
+   *
+   * @since 0.8
+   */
+  public static StatsRecorder getStatsRecorder() {
+    return statsComponent.getStatsRecorder();
+  }
+
+  /**
+   * Returns the default {@link ViewManager}.
+   *
+   * @since 0.8
+   */
+  public static ViewManager getViewManager() {
+    return statsComponent.getViewManager();
+  }
+
+  /**
+   * Returns the current {@code StatsCollectionState}.
+   *
+   * <p>When no implementation is available, {@code getState} always returns {@link
+   * StatsCollectionState#DISABLED}.
+   *
+   * <p>Once {@link #getState()} is called, subsequent calls to {@link
+   * #setState(StatsCollectionState)} will throw an {@code IllegalStateException}.
+   *
+   * @return the current {@code StatsCollectionState}.
+   * @since 0.8
+   */
+  public static StatsCollectionState getState() {
+    return statsComponent.getState();
+  }
+
+  /**
+   * Sets the current {@code StatsCollectionState}.
+   *
+   * <p>When no implementation is available, {@code setState} does not change the state.
+   *
+   * <p>If state is set to {@link StatsCollectionState#DISABLED}, all stats that are previously
+   * recorded will be cleared.
+   *
+   * @param state the new {@code StatsCollectionState}.
+   * @throws IllegalStateException if {@link #getState()} was previously called.
+   * @deprecated This method is deprecated because other libraries could cache the result of {@link
+   *     #getState()}, use a stale value, and behave incorrectly. It is only safe to call early in
+   *     initialization. This method throws {@link IllegalStateException} after {@code getState()}
+   *     has been called, in order to limit changes to the result of {@code getState()}.
+   * @since 0.8
+   */
+  @Deprecated
+  public static void setState(StatsCollectionState state) {
+    statsComponent.setState(state);
+  }
+
+  // Any provider that may be used for StatsComponent can be added here.
+  @DefaultVisibilityForTesting
+  static StatsComponent loadStatsComponent(@Nullable ClassLoader classLoader) {
+    try {
+      // Call Class.forName with literal string name of the class to help shading tools.
+      return Provider.createInstance(
+          Class.forName(
+              "io.opencensus.impl.stats.StatsComponentImpl", /*initialize=*/ true, classLoader),
+          StatsComponent.class);
+    } catch (ClassNotFoundException e) {
+      logger.log(
+          Level.FINE,
+          "Couldn't load full implementation for StatsComponent, now trying to load lite "
+              + "implementation.",
+          e);
+    }
+    try {
+      // Call Class.forName with literal string name of the class to help shading tools.
+      return Provider.createInstance(
+          Class.forName(
+              "io.opencensus.impllite.stats.StatsComponentImplLite",
+              /*initialize=*/ true,
+              classLoader),
+          StatsComponent.class);
+    } catch (ClassNotFoundException e) {
+      logger.log(
+          Level.FINE,
+          "Couldn't load lite implementation for StatsComponent, now using "
+              + "default implementation for StatsComponent.",
+          e);
+    }
+    return NoopStats.newNoopStatsComponent();
+  }
+
+  private Stats() {}
+}
diff --git a/api/src/main/java/io/opencensus/stats/StatsCollectionState.java b/api/src/main/java/io/opencensus/stats/StatsCollectionState.java
new file mode 100644
index 0000000..6b2f240
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/StatsCollectionState.java
@@ -0,0 +1,44 @@
+/*
+ * 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.stats;
+
+/**
+ * State of the {@link StatsComponent}.
+ *
+ * @since 0.8
+ */
+public enum StatsCollectionState {
+
+  /**
+   * State that fully enables stats collection.
+   *
+   * <p>The {@link StatsComponent} collects stats for registered views.
+   *
+   * @since 0.8
+   */
+  ENABLED,
+
+  /**
+   * State that disables stats collection.
+   *
+   * <p>The {@link StatsComponent} does not need to collect stats for registered views and may
+   * return empty {@link ViewData}s from {@link ViewManager#getView(View.Name)}.
+   *
+   * @since 0.8
+   */
+  DISABLED
+}
diff --git a/api/src/main/java/io/opencensus/stats/StatsComponent.java b/api/src/main/java/io/opencensus/stats/StatsComponent.java
new file mode 100644
index 0000000..9764fce
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/StatsComponent.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.stats;
+
+/**
+ * Class that holds the implementations for {@link ViewManager} and {@link StatsRecorder}.
+ *
+ * <p>All objects returned by methods on {@code StatsComponent} are cacheable.
+ *
+ * @since 0.8
+ */
+public abstract class StatsComponent {
+
+  /**
+   * Returns the default {@link ViewManager}.
+   *
+   * @since 0.8
+   */
+  public abstract ViewManager getViewManager();
+
+  /**
+   * Returns the default {@link StatsRecorder}.
+   *
+   * @since 0.8
+   */
+  public abstract StatsRecorder getStatsRecorder();
+
+  /**
+   * Returns the current {@code StatsCollectionState}.
+   *
+   * <p>When no implementation is available, {@code getState} always returns {@link
+   * StatsCollectionState#DISABLED}.
+   *
+   * <p>Once {@link #getState()} is called, subsequent calls to {@link
+   * #setState(StatsCollectionState)} will throw an {@code IllegalStateException}.
+   *
+   * @return the current {@code StatsCollectionState}.
+   * @since 0.8
+   */
+  public abstract StatsCollectionState getState();
+
+  /**
+   * Sets the current {@code StatsCollectionState}.
+   *
+   * <p>When no implementation is available, {@code setState} does not change the state.
+   *
+   * <p>If state is set to {@link StatsCollectionState#DISABLED}, all stats that are previously
+   * recorded will be cleared.
+   *
+   * @param state the new {@code StatsCollectionState}.
+   * @throws IllegalStateException if {@link #getState()} was previously called.
+   * @deprecated This method is deprecated because other libraries could cache the result of {@link
+   *     #getState()}, use a stale value, and behave incorrectly. It is only safe to call early in
+   *     initialization. This method throws {@link IllegalStateException} after {@code getState()}
+   *     has been called, in order to limit changes to the result of {@code getState()}.
+   * @since 0.8
+   */
+  @Deprecated
+  public abstract void setState(StatsCollectionState state);
+}
diff --git a/api/src/main/java/io/opencensus/stats/StatsRecorder.java b/api/src/main/java/io/opencensus/stats/StatsRecorder.java
new file mode 100644
index 0000000..87b8c8b
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/StatsRecorder.java
@@ -0,0 +1,34 @@
+/*
+ * 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.stats;
+
+/**
+ * Provides methods to record stats against tags.
+ *
+ * @since 0.8
+ */
+public abstract class StatsRecorder {
+  // TODO(sebright): Should we provide convenience methods for only recording one measure?
+
+  /**
+   * Returns an object for recording multiple measurements.
+   *
+   * @return an object for recording multiple measurements.
+   * @since 0.8
+   */
+  public abstract MeasureMap newMeasureMap();
+}
diff --git a/api/src/main/java/io/opencensus/stats/View.java b/api/src/main/java/io/opencensus/stats/View.java
new file mode 100644
index 0000000..f563ff9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/View.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2016-17, 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.StringUtils;
+import io.opencensus.internal.Utils;
+import io.opencensus.tags.TagKey;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A View specifies an aggregation and a set of tag keys. The aggregation will be broken down by the
+ * unique set of matching tag values for each measure.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+@AutoValue.CopyAnnotations
+@SuppressWarnings("deprecation")
+public abstract class View {
+
+  @DefaultVisibilityForTesting static final int NAME_MAX_LENGTH = 255;
+
+  private static final Comparator<TagKey> TAG_KEY_COMPARATOR =
+      new Comparator<TagKey>() {
+        @Override
+        public int compare(TagKey key1, TagKey key2) {
+          return key1.getName().compareTo(key2.getName());
+        }
+      };
+
+  View() {}
+
+  /**
+   * Name of view. Must be unique.
+   *
+   * @since 0.8
+   */
+  public abstract Name getName();
+
+  /**
+   * More detailed description, for documentation purposes.
+   *
+   * @since 0.8
+   */
+  public abstract String getDescription();
+
+  /**
+   * Measure type of this view.
+   *
+   * @since 0.8
+   */
+  public abstract Measure getMeasure();
+
+  /**
+   * The {@link Aggregation} associated with this {@link View}.
+   *
+   * @since 0.8
+   */
+  public abstract Aggregation getAggregation();
+
+  /**
+   * Columns (a.k.a Tag Keys) to match with the associated {@link Measure}.
+   *
+   * <p>{@link Measure} will be recorded in a "greedy" way. That is, every view aggregates every
+   * measure. This is similar to doing a GROUPBY on view’s columns. Columns must be unique.
+   *
+   * @since 0.8
+   */
+  public abstract List<TagKey> getColumns();
+
+  /**
+   * Returns the time {@link AggregationWindow} for this {@code View}.
+   *
+   * @return the time {@link AggregationWindow}.
+   * @since 0.8
+   * @deprecated since 0.13. In the future all {@link View}s will be cumulative.
+   */
+  @Deprecated
+  public abstract AggregationWindow getWindow();
+
+  /**
+   * Constructs a new {@link View}.
+   *
+   * @param name the {@link Name} of view. Must be unique.
+   * @param description the description of view.
+   * @param measure the {@link Measure} to be aggregated by this view.
+   * @param aggregation the basic {@link Aggregation} that this view will support.
+   * @param columns the {@link TagKey}s that this view will aggregate on. Columns should not contain
+   *     duplicates.
+   * @param window the {@link AggregationWindow} of view.
+   * @return a new {@link View}.
+   * @since 0.8
+   * @deprecated in favor of {@link #create(Name, String, Measure, Aggregation, List)}.
+   */
+  @Deprecated
+  public static View create(
+      Name name,
+      String description,
+      Measure measure,
+      Aggregation aggregation,
+      List<TagKey> columns,
+      AggregationWindow window) {
+    Utils.checkArgument(
+        new HashSet<TagKey>(columns).size() == columns.size(), "Columns have duplicate.");
+
+    List<TagKey> tagKeys = new ArrayList<TagKey>(columns);
+    Collections.sort(tagKeys, TAG_KEY_COMPARATOR);
+    return new AutoValue_View(
+        name, description, measure, aggregation, Collections.unmodifiableList(tagKeys), window);
+  }
+
+  /**
+   * Constructs a new {@link View}.
+   *
+   * @param name the {@link Name} of view. Must be unique.
+   * @param description the description of view.
+   * @param measure the {@link Measure} to be aggregated by this view.
+   * @param aggregation the basic {@link Aggregation} that this view will support.
+   * @param columns the {@link TagKey}s that this view will aggregate on. Columns should not contain
+   *     duplicates.
+   * @return a new {@link View}.
+   * @since 0.13
+   */
+  public static View create(
+      Name name,
+      String description,
+      Measure measure,
+      Aggregation aggregation,
+      List<TagKey> columns) {
+    Utils.checkArgument(
+        new HashSet<TagKey>(columns).size() == columns.size(), "Columns have duplicate.");
+    return create(
+        name, description, measure, aggregation, columns, AggregationWindow.Cumulative.create());
+  }
+
+  /**
+   * The name of a {@code View}.
+   *
+   * @since 0.8
+   */
+  // This type should be used as the key when associating data with Views.
+  @Immutable
+  @AutoValue
+  public abstract static class Name {
+
+    Name() {}
+
+    /**
+     * Returns the name as a {@code String}.
+     *
+     * @return the name as a {@code String}.
+     * @since 0.8
+     */
+    public abstract String asString();
+
+    /**
+     * Creates a {@code View.Name} from a {@code String}. Should be a ASCII string with a length no
+     * greater than 255 characters.
+     *
+     * <p>Suggested format for name: {@code <web_host>/<path>}.
+     *
+     * @param name the name {@code String}.
+     * @return a {@code View.Name} with the given name {@code String}.
+     * @since 0.8
+     */
+    public static Name create(String name) {
+      Utils.checkArgument(
+          StringUtils.isPrintableString(name) && name.length() <= NAME_MAX_LENGTH,
+          "Name should be a ASCII string with a length no greater than 255 characters.");
+      return new AutoValue_View_Name(name);
+    }
+  }
+
+  /**
+   * The time window for a {@code View}.
+   *
+   * @since 0.8
+   * @deprecated since 0.13. In the future all {@link View}s will be cumulative.
+   */
+  @Deprecated
+  @Immutable
+  public abstract static class AggregationWindow {
+
+    private AggregationWindow() {}
+
+    /**
+     * Applies the given match function to the underlying data type.
+     *
+     * @since 0.8
+     */
+    public abstract <T> T match(
+        Function<? super Cumulative, T> p0,
+        Function<? super Interval, T> p1,
+        Function<? super AggregationWindow, T> defaultFunction);
+
+    /**
+     * Cumulative (infinite interval) time {@code AggregationWindow}.
+     *
+     * @since 0.8
+     * @deprecated since 0.13. In the future all {@link View}s will be cumulative.
+     */
+    @Deprecated
+    @Immutable
+    @AutoValue
+    @AutoValue.CopyAnnotations
+    public abstract static class Cumulative extends AggregationWindow {
+
+      private static final Cumulative CUMULATIVE =
+          new AutoValue_View_AggregationWindow_Cumulative();
+
+      Cumulative() {}
+
+      /**
+       * Constructs a cumulative {@code AggregationWindow} that does not have an explicit {@code
+       * Duration}. Instead, cumulative {@code AggregationWindow} always has an interval of infinite
+       * {@code Duration}.
+       *
+       * @return a cumulative {@code AggregationWindow}.
+       * @since 0.8
+       */
+      public static Cumulative create() {
+        return CUMULATIVE;
+      }
+
+      @Override
+      public final <T> T match(
+          Function<? super Cumulative, T> p0,
+          Function<? super Interval, T> p1,
+          Function<? super AggregationWindow, T> defaultFunction) {
+        return p0.apply(this);
+      }
+    }
+
+    /**
+     * Interval (finite interval) time {@code AggregationWindow}.
+     *
+     * @since 0.8
+     * @deprecated since 0.13. In the future all {@link View}s will be cumulative.
+     */
+    @Deprecated
+    @Immutable
+    @AutoValue
+    @AutoValue.CopyAnnotations
+    public abstract static class Interval extends AggregationWindow {
+
+      private static final Duration ZERO = Duration.create(0, 0);
+
+      Interval() {}
+
+      /**
+       * Returns the {@code Duration} associated with this {@code Interval}.
+       *
+       * @return a {@code Duration}.
+       * @since 0.8
+       */
+      public abstract Duration getDuration();
+
+      /**
+       * Constructs an interval {@code AggregationWindow} that has a finite explicit {@code
+       * Duration}.
+       *
+       * <p>The {@code Duration} should be able to round to milliseconds. Currently interval window
+       * cannot have smaller {@code Duration} such as microseconds or nanoseconds.
+       *
+       * @return an interval {@code AggregationWindow}.
+       * @since 0.8
+       */
+      public static Interval create(Duration duration) {
+        Utils.checkArgument(duration.compareTo(ZERO) > 0, "Duration must be positive");
+        return new AutoValue_View_AggregationWindow_Interval(duration);
+      }
+
+      @Override
+      public final <T> T match(
+          Function<? super Cumulative, T> p0,
+          Function<? super Interval, T> p1,
+          Function<? super AggregationWindow, T> defaultFunction) {
+        return p1.apply(this);
+      }
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/stats/ViewData.java b/api/src/main/java/io/opencensus/stats/ViewData.java
new file mode 100644
index 0000000..df6edaa
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/ViewData.java
@@ -0,0 +1,461 @@
+/*
+ * Copyright 2016-17, 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.concurrent.Immutable;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * The aggregated data for a particular {@link View}.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+@AutoValue.CopyAnnotations
+@SuppressWarnings("deprecation")
+public abstract class ViewData {
+
+  // Prevents this class from being subclassed anywhere else.
+  ViewData() {}
+
+  /**
+   * The {@link View} associated with this {@link ViewData}.
+   *
+   * @since 0.8
+   */
+  public abstract View getView();
+
+  /**
+   * The {@link AggregationData} grouped by combination of tag values, associated with this {@link
+   * ViewData}.
+   *
+   * @since 0.8
+   */
+  public abstract Map<List</*@Nullable*/ TagValue>, AggregationData> getAggregationMap();
+
+  /**
+   * Returns the {@link AggregationWindowData} associated with this {@link ViewData}.
+   *
+   * <p>{@link AggregationWindowData} is deprecated since 0.13, please avoid using this method. Use
+   * {@link #getStart()} and {@link #getEnd()} instead.
+   *
+   * @return the {@code AggregationWindowData}.
+   * @since 0.8
+   * @deprecated in favor of {@link #getStart()} and {@link #getEnd()}.
+   */
+  @Deprecated
+  public abstract AggregationWindowData getWindowData();
+
+  /**
+   * Returns the start {@code Timestamp} for a {@link ViewData}.
+   *
+   * @return the start {@code Timestamp}.
+   * @since 0.13
+   */
+  public abstract Timestamp getStart();
+
+  /**
+   * Returns the end {@code Timestamp} for a {@link ViewData}.
+   *
+   * @return the end {@code Timestamp}.
+   * @since 0.13
+   */
+  public abstract Timestamp getEnd();
+
+  /**
+   * Constructs a new {@link ViewData}.
+   *
+   * @param view the {@link View} associated with this {@link ViewData}.
+   * @param map the mapping from {@link TagValue} list to {@link AggregationData}.
+   * @param windowData the {@link AggregationWindowData}.
+   * @return a {@code ViewData}.
+   * @throws IllegalArgumentException if the types of {@code Aggregation} and {@code
+   *     AggregationData} don't match, or the types of {@code Window} and {@code WindowData} don't
+   *     match.
+   * @since 0.8
+   * @deprecated in favor of {@link #create(View, Map, Timestamp, Timestamp)}.
+   */
+  @Deprecated
+  public static ViewData create(
+      final View view,
+      Map<? extends List</*@Nullable*/ TagValue>, ? extends AggregationData> map,
+      final AggregationWindowData windowData) {
+    checkWindow(view.getWindow(), windowData);
+    final Map<List</*@Nullable*/ TagValue>, AggregationData> deepCopy =
+        new HashMap<List</*@Nullable*/ TagValue>, AggregationData>();
+    for (Entry<? extends List</*@Nullable*/ TagValue>, ? extends AggregationData> entry :
+        map.entrySet()) {
+      checkAggregation(view.getAggregation(), entry.getValue(), view.getMeasure());
+      deepCopy.put(
+          Collections.unmodifiableList(new ArrayList</*@Nullable*/ TagValue>(entry.getKey())),
+          entry.getValue());
+    }
+    return windowData.match(
+        new Function<ViewData.AggregationWindowData.CumulativeData, ViewData>() {
+          @Override
+          public ViewData apply(ViewData.AggregationWindowData.CumulativeData arg) {
+            return createInternal(
+                view, Collections.unmodifiableMap(deepCopy), arg, arg.getStart(), arg.getEnd());
+          }
+        },
+        new Function<ViewData.AggregationWindowData.IntervalData, ViewData>() {
+          @Override
+          public ViewData apply(ViewData.AggregationWindowData.IntervalData arg) {
+            Duration duration = ((View.AggregationWindow.Interval) view.getWindow()).getDuration();
+            return createInternal(
+                view,
+                Collections.unmodifiableMap(deepCopy),
+                arg,
+                arg.getEnd()
+                    .addDuration(Duration.create(-duration.getSeconds(), -duration.getNanos())),
+                arg.getEnd());
+          }
+        },
+        Functions.<ViewData>throwAssertionError());
+  }
+
+  /**
+   * Constructs a new {@link ViewData}.
+   *
+   * @param view the {@link View} associated with this {@link ViewData}.
+   * @param map the mapping from {@link TagValue} list to {@link AggregationData}.
+   * @param start the start {@link Timestamp} for this {@link ViewData}.
+   * @param end the end {@link Timestamp} for this {@link ViewData}.
+   * @return a {@code ViewData}.
+   * @throws IllegalArgumentException if the types of {@code Aggregation} and {@code
+   *     AggregationData} don't match.
+   * @since 0.13
+   */
+  public static ViewData create(
+      View view,
+      Map<? extends List</*@Nullable*/ TagValue>, ? extends AggregationData> map,
+      Timestamp start,
+      Timestamp end) {
+    Map<List</*@Nullable*/ TagValue>, AggregationData> deepCopy =
+        new HashMap<List</*@Nullable*/ TagValue>, AggregationData>();
+    for (Entry<? extends List</*@Nullable*/ TagValue>, ? extends AggregationData> entry :
+        map.entrySet()) {
+      checkAggregation(view.getAggregation(), entry.getValue(), view.getMeasure());
+      deepCopy.put(
+          Collections.unmodifiableList(new ArrayList</*@Nullable*/ TagValue>(entry.getKey())),
+          entry.getValue());
+    }
+    return createInternal(
+        view,
+        Collections.unmodifiableMap(deepCopy),
+        AggregationWindowData.CumulativeData.create(start, end),
+        start,
+        end);
+  }
+
+  // Suppresses a nullness warning about calls to the AutoValue_ViewData constructor. The generated
+  // constructor does not have the @Nullable annotation on TagValue.
+  private static ViewData createInternal(
+      View view,
+      Map<List</*@Nullable*/ TagValue>, AggregationData> aggregationMap,
+      AggregationWindowData window,
+      Timestamp start,
+      Timestamp end) {
+    @SuppressWarnings("nullness")
+    Map<List<TagValue>, AggregationData> map = aggregationMap;
+    return new AutoValue_ViewData(view, map, window, start, end);
+  }
+
+  private static void checkWindow(
+      View.AggregationWindow window, final AggregationWindowData windowData) {
+    window.match(
+        new Function<View.AggregationWindow.Cumulative, Void>() {
+          @Override
+          public Void apply(View.AggregationWindow.Cumulative arg) {
+            throwIfWindowMismatch(
+                windowData instanceof AggregationWindowData.CumulativeData, arg, windowData);
+            return null;
+          }
+        },
+        new Function<View.AggregationWindow.Interval, Void>() {
+          @Override
+          public Void apply(View.AggregationWindow.Interval arg) {
+            throwIfWindowMismatch(
+                windowData instanceof AggregationWindowData.IntervalData, arg, windowData);
+            return null;
+          }
+        },
+        Functions.</*@Nullable*/ Void>throwAssertionError());
+  }
+
+  private static void throwIfWindowMismatch(
+      boolean isValid, View.AggregationWindow window, AggregationWindowData windowData) {
+    if (!isValid) {
+      throw new IllegalArgumentException(createErrorMessageForWindow(window, windowData));
+    }
+  }
+
+  private static String createErrorMessageForWindow(
+      View.AggregationWindow window, AggregationWindowData windowData) {
+    return "AggregationWindow and AggregationWindowData types mismatch. "
+        + "AggregationWindow: "
+        + window.getClass().getSimpleName()
+        + " AggregationWindowData: "
+        + windowData.getClass().getSimpleName();
+  }
+
+  private static void checkAggregation(
+      final Aggregation aggregation, final AggregationData aggregationData, final Measure measure) {
+    aggregation.match(
+        new Function<Sum, Void>() {
+          @Override
+          public Void apply(Sum arg) {
+            measure.match(
+                new Function<MeasureDouble, Void>() {
+                  @Override
+                  public Void apply(MeasureDouble arg) {
+                    throwIfAggregationMismatch(
+                        aggregationData instanceof SumDataDouble, aggregation, aggregationData);
+                    return null;
+                  }
+                },
+                new Function<MeasureLong, Void>() {
+                  @Override
+                  public Void apply(MeasureLong arg) {
+                    throwIfAggregationMismatch(
+                        aggregationData instanceof SumDataLong, aggregation, aggregationData);
+                    return null;
+                  }
+                },
+                Functions.</*@Nullable*/ Void>throwAssertionError());
+            return null;
+          }
+        },
+        new Function<Count, Void>() {
+          @Override
+          public Void apply(Count arg) {
+            throwIfAggregationMismatch(
+                aggregationData instanceof CountData, aggregation, aggregationData);
+            return null;
+          }
+        },
+        new Function<Distribution, Void>() {
+          @Override
+          public Void apply(Distribution arg) {
+            throwIfAggregationMismatch(
+                aggregationData instanceof DistributionData, aggregation, aggregationData);
+            return null;
+          }
+        },
+        new Function<LastValue, Void>() {
+          @Override
+          public Void apply(LastValue arg) {
+            measure.match(
+                new Function<MeasureDouble, Void>() {
+                  @Override
+                  public Void apply(MeasureDouble arg) {
+                    throwIfAggregationMismatch(
+                        aggregationData instanceof LastValueDataDouble,
+                        aggregation,
+                        aggregationData);
+                    return null;
+                  }
+                },
+                new Function<MeasureLong, Void>() {
+                  @Override
+                  public Void apply(MeasureLong arg) {
+                    throwIfAggregationMismatch(
+                        aggregationData instanceof LastValueDataLong, aggregation, aggregationData);
+                    return null;
+                  }
+                },
+                Functions.</*@Nullable*/ Void>throwAssertionError());
+            return null;
+          }
+        },
+        new Function<Aggregation, Void>() {
+          @Override
+          public Void apply(Aggregation arg) {
+            // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+            // we need to continue supporting Mean, since it could still be used by users and some
+            // deprecated RPC views.
+            if (arg instanceof Aggregation.Mean) {
+              throwIfAggregationMismatch(
+                  aggregationData instanceof AggregationData.MeanData,
+                  aggregation,
+                  aggregationData);
+              return null;
+            }
+            throw new AssertionError();
+          }
+        });
+  }
+
+  private static void throwIfAggregationMismatch(
+      boolean isValid, Aggregation aggregation, AggregationData aggregationData) {
+    if (!isValid) {
+      throw new IllegalArgumentException(
+          createErrorMessageForAggregation(aggregation, aggregationData));
+    }
+  }
+
+  private static String createErrorMessageForAggregation(
+      Aggregation aggregation, AggregationData aggregationData) {
+    return "Aggregation and AggregationData types mismatch. "
+        + "Aggregation: "
+        + aggregation.getClass().getSimpleName()
+        + " AggregationData: "
+        + aggregationData.getClass().getSimpleName();
+  }
+
+  /**
+   * The {@code AggregationWindowData} for a {@link ViewData}.
+   *
+   * @since 0.8
+   * @deprecated since 0.13, please use start and end {@link Timestamp} instead.
+   */
+  @Deprecated
+  @Immutable
+  public abstract static class AggregationWindowData {
+
+    private AggregationWindowData() {}
+
+    /**
+     * Applies the given match function to the underlying data type.
+     *
+     * @since 0.8
+     */
+    public abstract <T> T match(
+        Function<? super CumulativeData, T> p0,
+        Function<? super IntervalData, T> p1,
+        Function<? super AggregationWindowData, T> defaultFunction);
+
+    /**
+     * Cumulative {@code AggregationWindowData}.
+     *
+     * @since 0.8
+     * @deprecated since 0.13, please use start and end {@link Timestamp} instead.
+     */
+    @Deprecated
+    @Immutable
+    @AutoValue
+    @AutoValue.CopyAnnotations
+    public abstract static class CumulativeData extends AggregationWindowData {
+
+      CumulativeData() {}
+
+      /**
+       * Returns the start {@code Timestamp} for a {@link CumulativeData}.
+       *
+       * @return the start {@code Timestamp}.
+       * @since 0.8
+       */
+      public abstract Timestamp getStart();
+
+      /**
+       * Returns the end {@code Timestamp} for a {@link CumulativeData}.
+       *
+       * @return the end {@code Timestamp}.
+       * @since 0.8
+       */
+      public abstract Timestamp getEnd();
+
+      @Override
+      public final <T> T match(
+          Function<? super CumulativeData, T> p0,
+          Function<? super IntervalData, T> p1,
+          Function<? super AggregationWindowData, T> defaultFunction) {
+        return p0.apply(this);
+      }
+
+      /**
+       * Constructs a new {@link CumulativeData}.
+       *
+       * @since 0.8
+       */
+      public static CumulativeData create(Timestamp start, Timestamp end) {
+        if (start.compareTo(end) > 0) {
+          throw new IllegalArgumentException("Start time is later than end time.");
+        }
+        return new AutoValue_ViewData_AggregationWindowData_CumulativeData(start, end);
+      }
+    }
+
+    /**
+     * Interval {@code AggregationWindowData}.
+     *
+     * @since 0.8
+     * @deprecated since 0.13, please use start and end {@link Timestamp} instead.
+     */
+    @Deprecated
+    @Immutable
+    @AutoValue
+    @AutoValue.CopyAnnotations
+    public abstract static class IntervalData extends AggregationWindowData {
+
+      IntervalData() {}
+
+      /**
+       * Returns the end {@code Timestamp} for an {@link IntervalData}.
+       *
+       * @return the end {@code Timestamp}.
+       * @since 0.8
+       */
+      public abstract Timestamp getEnd();
+
+      @Override
+      public final <T> T match(
+          Function<? super CumulativeData, T> p0,
+          Function<? super IntervalData, T> p1,
+          Function<? super AggregationWindowData, T> defaultFunction) {
+        return p1.apply(this);
+      }
+
+      /**
+       * Constructs a new {@link IntervalData}.
+       *
+       * @since 0.8
+       */
+      public static IntervalData create(Timestamp end) {
+        return new AutoValue_ViewData_AggregationWindowData_IntervalData(end);
+      }
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/stats/ViewManager.java b/api/src/main/java/io/opencensus/stats/ViewManager.java
new file mode 100644
index 0000000..a00165c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/ViewManager.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016-17, 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.stats;
+
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/**
+ * Provides facilities to register {@link View}s for collecting stats and retrieving stats data as a
+ * {@link ViewData}.
+ *
+ * @since 0.8
+ */
+public abstract class ViewManager {
+  /**
+   * Pull model for stats. Registers a {@link View} that will collect data to be accessed via {@link
+   * #getView(View.Name)}.
+   *
+   * @param view the {@code View} to be registered.
+   * @since 0.8
+   */
+  public abstract void registerView(View view);
+
+  /**
+   * Returns the current stats data, {@link ViewData}, associated with the given view name.
+   *
+   * <p>Returns {@code null} if the {@code View} is not registered.
+   *
+   * @param view the name of {@code View} for the current stats.
+   * @return {@code ViewData} for the {@code View}, or {@code null} if the {@code View} is not
+   *     registered.
+   * @since 0.8
+   */
+  @Nullable
+  public abstract ViewData getView(View.Name view);
+
+  /**
+   * Returns all registered views that should be exported.
+   *
+   * <p>This method should be used by any stats exporter that automatically exports data for views
+   * registered with the {@link ViewManager}.
+   *
+   * @return all registered views that should be exported.
+   * @since 0.9
+   */
+  public abstract Set<View> getAllExportedViews();
+}
diff --git a/api/src/main/java/io/opencensus/stats/package-info.java b/api/src/main/java/io/opencensus/stats/package-info.java
new file mode 100644
index 0000000..981daa0
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/** API for stats recording. */
+// TODO: Add more details.
+// TODO: Add code examples.
+package io.opencensus.stats;
diff --git a/api/src/main/java/io/opencensus/tags/InternalUtils.java b/api/src/main/java/io/opencensus/tags/InternalUtils.java
new file mode 100644
index 0000000..944122e
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/InternalUtils.java
@@ -0,0 +1,38 @@
+/*
+ * 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.tags;
+
+import java.util.Iterator;
+
+/**
+ * Internal tagging utilities.
+ *
+ * @since 0.8
+ */
+@io.opencensus.common.Internal
+public final class InternalUtils {
+  private InternalUtils() {}
+
+  /**
+   * Internal tag accessor.
+   *
+   * @since 0.8
+   */
+  public static Iterator<Tag> getTags(TagContext tags) {
+    return tags.getIterator();
+  }
+}
diff --git a/api/src/main/java/io/opencensus/tags/NoopTags.java b/api/src/main/java/io/opencensus/tags/NoopTags.java
new file mode 100644
index 0000000..fb52b16
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/NoopTags.java
@@ -0,0 +1,214 @@
+/*
+ * 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.tags;
+
+import io.opencensus.common.Scope;
+import io.opencensus.internal.NoopScope;
+import io.opencensus.internal.Utils;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagPropagationComponent;
+import java.util.Collections;
+import java.util.Iterator;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** No-op implementations of tagging classes. */
+final class NoopTags {
+
+  private NoopTags() {}
+
+  /**
+   * Returns a {@code TagsComponent} that has a no-op implementation for {@link Tagger}.
+   *
+   * @return a {@code TagsComponent} that has a no-op implementation for {@code Tagger}.
+   */
+  static TagsComponent newNoopTagsComponent() {
+    return new NoopTagsComponent();
+  }
+
+  /**
+   * Returns a {@code Tagger} that only produces {@link TagContext}s with no tags.
+   *
+   * @return a {@code Tagger} that only produces {@code TagContext}s with no tags.
+   */
+  static Tagger getNoopTagger() {
+    return NoopTagger.INSTANCE;
+  }
+
+  /**
+   * Returns a {@code TagContextBuilder} that ignores all calls to {@link TagContextBuilder#put}.
+   *
+   * @return a {@code TagContextBuilder} that ignores all calls to {@link TagContextBuilder#put}.
+   */
+  static TagContextBuilder getNoopTagContextBuilder() {
+    return NoopTagContextBuilder.INSTANCE;
+  }
+
+  /**
+   * Returns a {@code TagContext} that does not contain any tags.
+   *
+   * @return a {@code TagContext} that does not contain any tags.
+   */
+  static TagContext getNoopTagContext() {
+    return NoopTagContext.INSTANCE;
+  }
+
+  /** Returns a {@code TagPropagationComponent} that contains no-op serializers. */
+  static TagPropagationComponent getNoopTagPropagationComponent() {
+    return NoopTagPropagationComponent.INSTANCE;
+  }
+
+  /**
+   * Returns a {@code TagContextBinarySerializer} that serializes all {@code TagContext}s to zero
+   * bytes and deserializes all inputs to empty {@code TagContext}s.
+   */
+  static TagContextBinarySerializer getNoopTagContextBinarySerializer() {
+    return NoopTagContextBinarySerializer.INSTANCE;
+  }
+
+  @ThreadSafe
+  private static final class NoopTagsComponent extends TagsComponent {
+    private volatile boolean isRead;
+
+    @Override
+    public Tagger getTagger() {
+      return getNoopTagger();
+    }
+
+    @Override
+    public TagPropagationComponent getTagPropagationComponent() {
+      return getNoopTagPropagationComponent();
+    }
+
+    @Override
+    public TaggingState getState() {
+      isRead = true;
+      return TaggingState.DISABLED;
+    }
+
+    @Override
+    @Deprecated
+    public void setState(TaggingState state) {
+      Utils.checkNotNull(state, "state");
+      Utils.checkState(!isRead, "State was already read, cannot set state.");
+    }
+  }
+
+  @Immutable
+  private static final class NoopTagger extends Tagger {
+    static final Tagger INSTANCE = new NoopTagger();
+
+    @Override
+    public TagContext empty() {
+      return getNoopTagContext();
+    }
+
+    @Override
+    public TagContext getCurrentTagContext() {
+      return getNoopTagContext();
+    }
+
+    @Override
+    public TagContextBuilder emptyBuilder() {
+      return getNoopTagContextBuilder();
+    }
+
+    @Override
+    public TagContextBuilder toBuilder(TagContext tags) {
+      Utils.checkNotNull(tags, "tags");
+      return getNoopTagContextBuilder();
+    }
+
+    @Override
+    public TagContextBuilder currentBuilder() {
+      return getNoopTagContextBuilder();
+    }
+
+    @Override
+    public Scope withTagContext(TagContext tags) {
+      Utils.checkNotNull(tags, "tags");
+      return NoopScope.getInstance();
+    }
+  }
+
+  @Immutable
+  private static final class NoopTagContextBuilder extends TagContextBuilder {
+    static final TagContextBuilder INSTANCE = new NoopTagContextBuilder();
+
+    @Override
+    public TagContextBuilder put(TagKey key, TagValue value) {
+      Utils.checkNotNull(key, "key");
+      Utils.checkNotNull(value, "value");
+      return this;
+    }
+
+    @Override
+    public TagContextBuilder remove(TagKey key) {
+      Utils.checkNotNull(key, "key");
+      return this;
+    }
+
+    @Override
+    public TagContext build() {
+      return getNoopTagContext();
+    }
+
+    @Override
+    public Scope buildScoped() {
+      return NoopScope.getInstance();
+    }
+  }
+
+  @Immutable
+  private static final class NoopTagContext extends TagContext {
+    static final TagContext INSTANCE = new NoopTagContext();
+
+    // TODO(sebright): Is there any way to let the user know that their tags were ignored?
+    @Override
+    protected Iterator<Tag> getIterator() {
+      return Collections.<Tag>emptySet().iterator();
+    }
+  }
+
+  @Immutable
+  private static final class NoopTagPropagationComponent extends TagPropagationComponent {
+    static final TagPropagationComponent INSTANCE = new NoopTagPropagationComponent();
+
+    @Override
+    public TagContextBinarySerializer getBinarySerializer() {
+      return getNoopTagContextBinarySerializer();
+    }
+  }
+
+  @Immutable
+  private static final class NoopTagContextBinarySerializer extends TagContextBinarySerializer {
+    static final TagContextBinarySerializer INSTANCE = new NoopTagContextBinarySerializer();
+    static final byte[] EMPTY_BYTE_ARRAY = {};
+
+    @Override
+    public byte[] toByteArray(TagContext tags) {
+      Utils.checkNotNull(tags, "tags");
+      return EMPTY_BYTE_ARRAY;
+    }
+
+    @Override
+    public TagContext fromByteArray(byte[] bytes) {
+      Utils.checkNotNull(bytes, "bytes");
+      return getNoopTagContext();
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/tags/Tag.java b/api/src/main/java/io/opencensus/tags/Tag.java
new file mode 100644
index 0000000..9e0a7a8
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/Tag.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.tags;
+
+import com.google.auto.value.AutoValue;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link TagKey} paired with a {@link TagValue}.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+public abstract class Tag {
+
+  Tag() {}
+
+  /**
+   * Creates a {@code Tag} from the given key and value.
+   *
+   * @param key the tag key.
+   * @param value the tag value.
+   * @return a {@code Tag} with the given key and value.
+   * @since 0.8
+   */
+  public static Tag create(TagKey key, TagValue value) {
+    return new AutoValue_Tag(key, value);
+  }
+
+  /**
+   * Returns the tag's key.
+   *
+   * @return the tag's key.
+   * @since 0.8
+   */
+  public abstract TagKey getKey();
+
+  /**
+   * Returns the tag's value.
+   *
+   * @return the tag's value.
+   * @since 0.8
+   */
+  public abstract TagValue getValue();
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagContext.java b/api/src/main/java/io/opencensus/tags/TagContext.java
new file mode 100644
index 0000000..e36acdf
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagContext.java
@@ -0,0 +1,109 @@
+/*
+ * 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.tags;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A map from {@link TagKey} to {@link TagValue} that can be used to label anything that is
+ * associated with a specific operation.
+ *
+ * <p>For example, {@code TagContext}s can be used to label stats, log messages, or debugging
+ * information.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class TagContext {
+
+  /**
+   * Returns an iterator over the tags in this {@code TagContext}.
+   *
+   * @return an iterator over the tags in this {@code TagContext}.
+   * @since 0.8
+   */
+  // This method is protected to prevent client code from accessing the tags of any TagContext. We
+  // don't currently support efficient access to tags. However, every TagContext subclass needs to
+  // provide access to its tags to the stats and tagging implementations by implementing this
+  // method. If we decide to support access to tags in the future, we can add a public iterator()
+  // method and implement it for all subclasses by calling getIterator().
+  //
+  // The stats and tagging implementations can access any TagContext's tags through
+  // io.opencensus.tags.InternalUtils.getTags, which calls this method.
+  protected abstract Iterator<Tag> getIterator();
+
+  @Override
+  public String toString() {
+    return "TagContext";
+  }
+
+  /**
+   * Returns true iff the other object is an instance of {@code TagContext} and contains the same
+   * key-value pairs. Implementations are free to override this method to provide better
+   * performance.
+   */
+  @Override
+  public boolean equals(@Nullable Object other) {
+    if (!(other instanceof TagContext)) {
+      return false;
+    }
+    TagContext otherTags = (TagContext) other;
+    Iterator<Tag> iter1 = getIterator();
+    Iterator<Tag> iter2 = otherTags.getIterator();
+    HashMap<Tag, Integer> tags = new HashMap<Tag, Integer>();
+    while (iter1 != null && iter1.hasNext()) {
+      Tag tag = iter1.next();
+      if (tags.containsKey(tag)) {
+        tags.put(tag, tags.get(tag) + 1);
+      } else {
+        tags.put(tag, 1);
+      }
+    }
+    while (iter2 != null && iter2.hasNext()) {
+      Tag tag = iter2.next();
+      if (!tags.containsKey(tag)) {
+        return false;
+      }
+      int count = tags.get(tag);
+      if (count > 1) {
+        tags.put(tag, count - 1);
+      } else {
+        tags.remove(tag);
+      }
+    }
+    return tags.isEmpty();
+  }
+
+  @Override
+  public final int hashCode() {
+    int hashCode = 0;
+    Iterator<Tag> i = getIterator();
+    if (i == null) {
+      return hashCode;
+    }
+    while (i.hasNext()) {
+      Tag tag = i.next();
+      if (tag != null) {
+        hashCode += tag.hashCode();
+      }
+    }
+    return hashCode;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagContextBuilder.java b/api/src/main/java/io/opencensus/tags/TagContextBuilder.java
new file mode 100644
index 0000000..f426896
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagContextBuilder.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.tags;
+
+import io.opencensus.common.Scope;
+
+/**
+ * Builder for the {@link TagContext} class.
+ *
+ * @since 0.8
+ */
+public abstract class TagContextBuilder {
+
+  /**
+   * Adds the key/value pair regardless of whether the key is present.
+   *
+   * @param key the {@code TagKey} which will be set.
+   * @param value the {@code TagValue} to set for the given key.
+   * @return this
+   * @since 0.8
+   */
+  public abstract TagContextBuilder put(TagKey key, TagValue value);
+
+  /**
+   * Removes the key if it exists.
+   *
+   * @param key the {@code TagKey} which will be removed.
+   * @return this
+   * @since 0.8
+   */
+  public abstract TagContextBuilder remove(TagKey key);
+
+  /**
+   * Creates a {@code TagContext} from this builder.
+   *
+   * @return a {@code TagContext} with the same tags as this builder.
+   * @since 0.8
+   */
+  public abstract TagContext build();
+
+  /**
+   * Enters the scope of code where the {@link TagContext} created from this builder is in the
+   * current context and returns an object that represents that scope. The scope is exited when the
+   * returned object is closed.
+   *
+   * @return an object that defines a scope where the {@code TagContext} created from this builder
+   *     is set to the current context.
+   * @since 0.8
+   */
+  public abstract Scope buildScoped();
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagKey.java b/api/src/main/java/io/opencensus/tags/TagKey.java
new file mode 100644
index 0000000..ca4582b
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagKey.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.tags;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.StringUtils;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A key to a value stored in a {@link TagContext}.
+ *
+ * <p>Each {@code TagKey} has a {@code String} name. Names have a maximum length of {@link
+ * #MAX_LENGTH} and contain only printable ASCII characters.
+ *
+ * <p>{@code TagKey}s are designed to be used as constants. Declaring each key as a constant
+ * prevents key names from being validated multiple times.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+public abstract class TagKey {
+  /**
+   * The maximum length for a tag key name. The value is {@value #MAX_LENGTH}.
+   *
+   * @since 0.8
+   */
+  public static final int MAX_LENGTH = 255;
+
+  TagKey() {}
+
+  /**
+   * Constructs a {@code TagKey} with the given name.
+   *
+   * <p>The name must meet the following requirements:
+   *
+   * <ol>
+   *   <li>It cannot be longer than {@link #MAX_LENGTH}.
+   *   <li>It can only contain printable ASCII characters.
+   * </ol>
+   *
+   * @param name the name of the key.
+   * @return a {@code TagKey} with the given name.
+   * @throws IllegalArgumentException if the name is not valid.
+   * @since 0.8
+   */
+  public static TagKey create(String name) {
+    Utils.checkArgument(isValid(name), "Invalid TagKey name: %s", name);
+    return new AutoValue_TagKey(name);
+  }
+
+  /**
+   * Returns the name of the key.
+   *
+   * @return the name of the key.
+   * @since 0.8
+   */
+  public abstract String getName();
+
+  /**
+   * Determines whether the given {@code String} is a valid tag key.
+   *
+   * @param name the tag key name to be validated.
+   * @return whether the name is valid.
+   */
+  private static boolean isValid(String name) {
+    return !name.isEmpty() && name.length() <= MAX_LENGTH && StringUtils.isPrintableString(name);
+  }
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagValue.java b/api/src/main/java/io/opencensus/tags/TagValue.java
new file mode 100644
index 0000000..9111ca2
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagValue.java
@@ -0,0 +1,79 @@
+/*
+ * 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.tags;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.StringUtils;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A validated tag value.
+ *
+ * <p>Validation ensures that the {@code String} has a maximum length of {@link #MAX_LENGTH} and
+ * contains only printable ASCII characters.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+public abstract class TagValue {
+  /**
+   * The maximum length for a tag value. The value is {@value #MAX_LENGTH}.
+   *
+   * @since 0.8
+   */
+  public static final int MAX_LENGTH = 255;
+
+  TagValue() {}
+
+  /**
+   * Constructs a {@code TagValue} from the given string. The string must meet the following
+   * requirements:
+   *
+   * <ol>
+   *   <li>It cannot be longer than {@link #MAX_LENGTH}.
+   *   <li>It can only contain printable ASCII characters.
+   * </ol>
+   *
+   * @param value the tag value.
+   * @throws IllegalArgumentException if the {@code String} is not valid.
+   * @since 0.8
+   */
+  public static TagValue create(String value) {
+    Utils.checkArgument(isValid(value), "Invalid TagValue: %s", value);
+    return new AutoValue_TagValue(value);
+  }
+
+  /**
+   * Returns the tag value as a {@code String}.
+   *
+   * @return the tag value as a {@code String}.
+   * @since 0.8
+   */
+  public abstract String asString();
+
+  /**
+   * Determines whether the given {@code String} is a valid tag value.
+   *
+   * @param value the tag value to be validated.
+   * @return whether the value is valid.
+   */
+  private static boolean isValid(String value) {
+    return value.length() <= MAX_LENGTH && StringUtils.isPrintableString(value);
+  }
+}
diff --git a/api/src/main/java/io/opencensus/tags/Tagger.java b/api/src/main/java/io/opencensus/tags/Tagger.java
new file mode 100644
index 0000000..f1e203a
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/Tagger.java
@@ -0,0 +1,86 @@
+/*
+ * 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.tags;
+
+import io.opencensus.common.Scope;
+
+/**
+ * Object for creating new {@link TagContext}s and {@code TagContext}s based on the current context.
+ *
+ * <p>This class returns {@link TagContextBuilder builders} that can be used to create the
+ * implementation-dependent {@link TagContext}s.
+ *
+ * <p>Implementations may have different constraints and are free to convert tag contexts to their
+ * own subtypes. This means callers cannot assume the {@link #getCurrentTagContext() current
+ * context} is the same instance as the one {@link #withTagContext(TagContext) placed into scope}.
+ *
+ * @since 0.8
+ */
+public abstract class Tagger {
+
+  /**
+   * Returns an empty {@code TagContext}.
+   *
+   * @return an empty {@code TagContext}.
+   * @since 0.8
+   */
+  public abstract TagContext empty();
+
+  /**
+   * Returns the current {@code TagContext}.
+   *
+   * @return the current {@code TagContext}.
+   * @since 0.8
+   */
+  public abstract TagContext getCurrentTagContext();
+
+  /**
+   * Returns a new empty {@code Builder}.
+   *
+   * @return a new empty {@code Builder}.
+   * @since 0.8
+   */
+  public abstract TagContextBuilder emptyBuilder();
+
+  /**
+   * Returns a builder based on this {@code TagContext}.
+   *
+   * @return a builder based on this {@code TagContext}.
+   * @since 0.8
+   */
+  public abstract TagContextBuilder toBuilder(TagContext tags);
+
+  /**
+   * Returns a new builder created from the current {@code TagContext}.
+   *
+   * @return a new builder created from the current {@code TagContext}.
+   * @since 0.8
+   */
+  public abstract TagContextBuilder currentBuilder();
+
+  /**
+   * Enters the scope of code where the given {@code TagContext} is in the current context
+   * (replacing the previous {@code TagContext}) and returns an object that represents that scope.
+   * The scope is exited when the returned object is closed.
+   *
+   * @param tags the {@code TagContext} to be set to the current context.
+   * @return an object that defines a scope where the given {@code TagContext} is set to the current
+   *     context.
+   * @since 0.8
+   */
+  public abstract Scope withTagContext(TagContext tags);
+}
diff --git a/api/src/main/java/io/opencensus/tags/TaggingState.java b/api/src/main/java/io/opencensus/tags/TaggingState.java
new file mode 100644
index 0000000..8897036
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TaggingState.java
@@ -0,0 +1,48 @@
+/*
+ * 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.tags;
+
+/**
+ * State of the {@link TagsComponent}.
+ *
+ * @since 0.8
+ */
+public enum TaggingState {
+  // TODO(sebright): Should we add a state that propagates the tags, but doesn't allow
+  // modifications?
+
+  /**
+   * State that fully enables tagging.
+   *
+   * <p>The {@link TagsComponent} can add tags to {@link TagContext}s, propagate {@code TagContext}s
+   * in the current context, and serialize {@code TagContext}s.
+   *
+   * @since 0.8
+   */
+  ENABLED,
+
+  /**
+   * State that disables tagging.
+   *
+   * <p>The {@link TagsComponent} may not add tags to {@link TagContext}s, propagate {@code
+   * TagContext}s in the current context, or serialize {@code TagContext}s.
+   *
+   * @since 0.8
+   */
+  // TODO(sebright): Document how this interacts with stats collection.
+  DISABLED
+}
diff --git a/api/src/main/java/io/opencensus/tags/Tags.java b/api/src/main/java/io/opencensus/tags/Tags.java
new file mode 100644
index 0000000..0712364
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/Tags.java
@@ -0,0 +1,126 @@
+/*
+ * 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.tags;
+
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Provider;
+import io.opencensus.tags.propagation.TagPropagationComponent;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * Class for accessing the default {@link TagsComponent}.
+ *
+ * @since 0.8
+ */
+public final class Tags {
+  private static final Logger logger = Logger.getLogger(Tags.class.getName());
+
+  private static final TagsComponent tagsComponent =
+      loadTagsComponent(TagsComponent.class.getClassLoader());
+
+  private Tags() {}
+
+  /**
+   * Returns the default {@code Tagger}.
+   *
+   * @return the default {@code Tagger}.
+   * @since 0.8
+   */
+  public static Tagger getTagger() {
+    return tagsComponent.getTagger();
+  }
+
+  /**
+   * Returns the default {@code TagPropagationComponent}.
+   *
+   * @return the default {@code TagPropagationComponent}.
+   * @since 0.8
+   */
+  public static TagPropagationComponent getTagPropagationComponent() {
+    return tagsComponent.getTagPropagationComponent();
+  }
+
+  /**
+   * Returns the current {@code TaggingState}.
+   *
+   * <p>When no implementation is available, {@code getState} always returns {@link
+   * TaggingState#DISABLED}.
+   *
+   * <p>Once {@link #getState()} is called, subsequent calls to {@link #setState(TaggingState)} will
+   * throw an {@code IllegalStateException}.
+   *
+   * @return the current {@code TaggingState}.
+   * @since 0.8
+   */
+  public static TaggingState getState() {
+    return tagsComponent.getState();
+  }
+
+  /**
+   * Sets the current {@code TaggingState}.
+   *
+   * <p>When no implementation is available, {@code setState} does not change the state.
+   *
+   * @param state the new {@code TaggingState}.
+   * @throws IllegalStateException if {@link #getState()} was previously called.
+   * @deprecated This method is deprecated because other libraries could cache the result of {@link
+   *     #getState()}, use a stale value, and behave incorrectly. It is only safe to call early in
+   *     initialization. This method throws {@link IllegalStateException} after {@link #getState()}
+   *     has been called, in order to limit changes to the result of {@code getState()}.
+   * @since 0.8
+   */
+  @Deprecated
+  public static void setState(TaggingState state) {
+    tagsComponent.setState(state);
+  }
+
+  // Any provider that may be used for TagsComponent can be added here.
+  @DefaultVisibilityForTesting
+  static TagsComponent loadTagsComponent(@Nullable ClassLoader classLoader) {
+    try {
+      // Call Class.forName with literal string name of the class to help shading tools.
+      return Provider.createInstance(
+          Class.forName(
+              "io.opencensus.impl.tags.TagsComponentImpl", /*initialize=*/ true, classLoader),
+          TagsComponent.class);
+    } catch (ClassNotFoundException e) {
+      logger.log(
+          Level.FINE,
+          "Couldn't load full implementation for TagsComponent, now trying to load lite "
+              + "implementation.",
+          e);
+    }
+    try {
+      // Call Class.forName with literal string name of the class to help shading tools.
+      return Provider.createInstance(
+          Class.forName(
+              "io.opencensus.impllite.tags.TagsComponentImplLite",
+              /*initialize=*/ true,
+              classLoader),
+          TagsComponent.class);
+    } catch (ClassNotFoundException e) {
+      logger.log(
+          Level.FINE,
+          "Couldn't load lite implementation for TagsComponent, now using "
+              + "default implementation for TagsComponent.",
+          e);
+    }
+    return NoopTags.newNoopTagsComponent();
+  }
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagsComponent.java b/api/src/main/java/io/opencensus/tags/TagsComponent.java
new file mode 100644
index 0000000..d34f195
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagsComponent.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.tags;
+
+import io.opencensus.tags.propagation.TagPropagationComponent;
+
+/**
+ * Class that holds the implementation for {@link Tagger} and {@link TagPropagationComponent}.
+ *
+ * <p>All objects returned by methods on {@code TagsComponent} are cacheable.
+ *
+ * @since 0.8
+ */
+public abstract class TagsComponent {
+
+  /**
+   * Returns the {@link Tagger} for this implementation.
+   *
+   * @since 0.8
+   */
+  public abstract Tagger getTagger();
+
+  /**
+   * Returns the {@link TagPropagationComponent} for this implementation.
+   *
+   * @since 0.8
+   */
+  public abstract TagPropagationComponent getTagPropagationComponent();
+
+  /**
+   * Returns the current {@code TaggingState}.
+   *
+   * <p>When no implementation is available, {@code getState} always returns {@link
+   * TaggingState#DISABLED}.
+   *
+   * <p>Once {@link #getState()} is called, subsequent calls to {@link #setState(TaggingState)} will
+   * throw an {@code IllegalStateException}.
+   *
+   * @return the current {@code TaggingState}.
+   * @since 0.8
+   */
+  public abstract TaggingState getState();
+
+  /**
+   * Sets the current {@code TaggingState}.
+   *
+   * <p>When no implementation is available, {@code setState} does not change the state.
+   *
+   * @param state the new {@code TaggingState}.
+   * @throws IllegalStateException if {@link #getState()} was previously called.
+   * @deprecated This method is deprecated because other libraries could cache the result of {@link
+   *     #getState()}, use a stale value, and behave incorrectly. It is only safe to call early in
+   *     initialization. This method throws {@link IllegalStateException} after {@code getState()}
+   *     has been called, in order to limit changes to the result of {@code getState()}.
+   * @since 0.8
+   */
+  @Deprecated
+  public abstract void setState(TaggingState state);
+}
diff --git a/api/src/main/java/io/opencensus/tags/package-info.java b/api/src/main/java/io/opencensus/tags/package-info.java
new file mode 100644
index 0000000..eb19ee7
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+/**
+ * API for associating tags with scoped operations.
+ *
+ * <p>This package manages a set of tags in the {@code io.grpc.Context}. The tags can be used to
+ * label anything that is associated with a specific operation. For example, the {@code
+ * io.opencensus.stats} package labels all stats with the current tags.
+ *
+ * <p>{@link io.opencensus.tags.Tag Tags} are key-value pairs. The {@link io.opencensus.tags.TagKey
+ * keys} and {@link io.opencensus.tags.TagValue values} are wrapped {@code String}s. They are stored
+ * as a map in a {@link io.opencensus.tags.TagContext}.
+ *
+ * <p>Note that tags are independent of the tracing data that is propagated in the {@code
+ * io.grpc.Context}, such as trace ID.
+ */
+// TODO(sebright): Add code examples.
+package io.opencensus.tags;
diff --git a/api/src/main/java/io/opencensus/tags/propagation/TagContextBinarySerializer.java b/api/src/main/java/io/opencensus/tags/propagation/TagContextBinarySerializer.java
new file mode 100644
index 0000000..39eb8ce
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/propagation/TagContextBinarySerializer.java
@@ -0,0 +1,57 @@
+/*
+ * 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.tags.propagation;
+
+import io.opencensus.tags.TagContext;
+
+/**
+ * Object for serializing and deserializing {@link TagContext}s with the binary format.
+ *
+ * <p>See <a
+ * href="https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/BinaryEncoding.md#tag-context">opencensus-specs</a>
+ * for the specification of the cross-language binary serialization format.
+ *
+ * @since 0.8
+ */
+public abstract class TagContextBinarySerializer {
+
+  /**
+   * Serializes the {@code TagContext} into the on-the-wire representation.
+   *
+   * <p>This method should be the inverse of {@link #fromByteArray}.
+   *
+   * @param tags the {@code TagContext} to serialize.
+   * @return the on-the-wire representation of a {@code TagContext}.
+   * @throws TagContextSerializationException if the result would be larger than the maximum allowed
+   *     serialized size.
+   * @since 0.8
+   */
+  public abstract byte[] toByteArray(TagContext tags) throws TagContextSerializationException;
+
+  /**
+   * Creates a {@code TagContext} from the given on-the-wire encoded representation.
+   *
+   * <p>This method should be the inverse of {@link #toByteArray}.
+   *
+   * @param bytes on-the-wire representation of a {@code TagContext}.
+   * @return a {@code TagContext} deserialized from {@code bytes}.
+   * @throws TagContextDeserializationException if there is a parse error, the input contains
+   *     invalid tags, or the input is larger than the maximum allowed serialized size.
+   * @since 0.8
+   */
+  public abstract TagContext fromByteArray(byte[] bytes) throws TagContextDeserializationException;
+}
diff --git a/api/src/main/java/io/opencensus/tags/propagation/TagContextDeserializationException.java b/api/src/main/java/io/opencensus/tags/propagation/TagContextDeserializationException.java
new file mode 100644
index 0000000..11dcb59
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/propagation/TagContextDeserializationException.java
@@ -0,0 +1,49 @@
+/*
+ * 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.tags.propagation;
+
+import io.opencensus.tags.TagContext;
+
+/**
+ * Exception thrown when a {@link TagContext} cannot be parsed.
+ *
+ * @since 0.8
+ */
+public final class TagContextDeserializationException extends Exception {
+  private static final long serialVersionUID = 0L;
+
+  /**
+   * Constructs a new {@code TagContextParseException} with the given message.
+   *
+   * @param message a message describing the error.
+   * @since 0.8
+   */
+  public TagContextDeserializationException(String message) {
+    super(message);
+  }
+
+  /**
+   * Constructs a new {@code TagContextParseException} with the given message and cause.
+   *
+   * @param message a message describing the error.
+   * @param cause the cause of the error.
+   * @since 0.8
+   */
+  public TagContextDeserializationException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/api/src/main/java/io/opencensus/tags/propagation/TagContextSerializationException.java b/api/src/main/java/io/opencensus/tags/propagation/TagContextSerializationException.java
new file mode 100644
index 0000000..bb3c9b7
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/propagation/TagContextSerializationException.java
@@ -0,0 +1,49 @@
+/*
+ * 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.tags.propagation;
+
+import io.opencensus.tags.TagContext;
+
+/**
+ * Exception thrown when a {@link TagContext} cannot be serialized.
+ *
+ * @since 0.8
+ */
+public final class TagContextSerializationException extends Exception {
+  private static final long serialVersionUID = 0L;
+
+  /**
+   * Constructs a new {@code TagContextSerializationException} with the given message.
+   *
+   * @param message a message describing the error.
+   * @since 0.8
+   */
+  public TagContextSerializationException(String message) {
+    super(message);
+  }
+
+  /**
+   * Constructs a new {@code TagContextSerializationException} with the given message and cause.
+   *
+   * @param message a message describing the error.
+   * @param cause the cause of the error.
+   * @since 0.8
+   */
+  public TagContextSerializationException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/api/src/main/java/io/opencensus/tags/propagation/TagPropagationComponent.java b/api/src/main/java/io/opencensus/tags/propagation/TagPropagationComponent.java
new file mode 100644
index 0000000..6ececa7
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/propagation/TagPropagationComponent.java
@@ -0,0 +1,36 @@
+/*
+ * 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.tags.propagation;
+
+import io.opencensus.tags.TagContext;
+
+/**
+ * Object containing all supported {@link TagContext} propagation formats.
+ *
+ * @since 0.8
+ */
+// TODO(sebright): Add an HTTP serializer.
+public abstract class TagPropagationComponent {
+
+  /**
+   * Returns the {@link TagContextBinarySerializer} for this implementation.
+   *
+   * @return the {@code TagContextBinarySerializer} for this implementation.
+   * @since 0.8
+   */
+  public abstract TagContextBinarySerializer getBinarySerializer();
+}
diff --git a/api/src/main/java/io/opencensus/tags/unsafe/ContextUtils.java b/api/src/main/java/io/opencensus/tags/unsafe/ContextUtils.java
new file mode 100644
index 0000000..8936bbb
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/unsafe/ContextUtils.java
@@ -0,0 +1,56 @@
+/*
+ * 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.tags.unsafe;
+
+import io.grpc.Context;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import java.util.Collections;
+import java.util.Iterator;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Utility methods for accessing the {@link TagContext} contained in the {@link io.grpc.Context}.
+ *
+ * <p>Most code should interact with the current context via the public APIs in {@link
+ * io.opencensus.tags.TagContext} and avoid accessing {@link #TAG_CONTEXT_KEY} directly.
+ *
+ * @since 0.8
+ */
+public final class ContextUtils {
+  private static final TagContext EMPTY_TAG_CONTEXT = new EmptyTagContext();
+
+  private ContextUtils() {}
+
+  /**
+   * The {@link io.grpc.Context.Key} used to interact with the {@code TagContext} contained in the
+   * {@link io.grpc.Context}.
+   *
+   * @since 0.8
+   */
+  public static final Context.Key<TagContext> TAG_CONTEXT_KEY =
+      Context.keyWithDefault("opencensus-tag-context-key", EMPTY_TAG_CONTEXT);
+
+  @Immutable
+  private static final class EmptyTagContext extends TagContext {
+
+    @Override
+    protected Iterator<Tag> getIterator() {
+      return Collections.<Tag>emptySet().iterator();
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/Annotation.java b/api/src/main/java/io/opencensus/trace/Annotation.java
new file mode 100644
index 0000000..97f2fdd
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Annotation.java
@@ -0,0 +1,83 @@
+/*
+ * 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A text annotation with a set of attributes.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class Annotation {
+  private static final Map<String, AttributeValue> EMPTY_ATTRIBUTES =
+      Collections.unmodifiableMap(Collections.<String, AttributeValue>emptyMap());
+
+  /**
+   * Returns a new {@code Annotation} with the given description.
+   *
+   * @param description the text description of the {@code Annotation}.
+   * @return a new {@code Annotation} with the given description.
+   * @throws NullPointerException if {@code description} is {@code null}.
+   * @since 0.5
+   */
+  public static Annotation fromDescription(String description) {
+    return new AutoValue_Annotation(description, EMPTY_ATTRIBUTES);
+  }
+
+  /**
+   * Returns a new {@code Annotation} with the given description and set of attributes.
+   *
+   * @param description the text description of the {@code Annotation}.
+   * @param attributes the attributes of the {@code Annotation}.
+   * @return a new {@code Annotation} with the given description and set of attributes.
+   * @throws NullPointerException if {@code description} or {@code attributes} are {@code null}.
+   * @since 0.5
+   */
+  public static Annotation fromDescriptionAndAttributes(
+      String description, Map<String, AttributeValue> attributes) {
+    return new AutoValue_Annotation(
+        description,
+        Collections.unmodifiableMap(
+            new HashMap<String, AttributeValue>(Utils.checkNotNull(attributes, "attributes"))));
+  }
+
+  /**
+   * Return the description of the {@code Annotation}.
+   *
+   * @return the description of the {@code Annotation}.
+   * @since 0.5
+   */
+  public abstract String getDescription();
+
+  /**
+   * Return the attributes of the {@code Annotation}.
+   *
+   * @return the attributes of the {@code Annotation}.
+   * @since 0.5
+   */
+  public abstract Map<String, AttributeValue> getAttributes();
+
+  Annotation() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/AttributeValue.java b/api/src/main/java/io/opencensus/trace/AttributeValue.java
new file mode 100644
index 0000000..efa9d1d
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/AttributeValue.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents all the possible values for an attribute. An attribute can have 3 types
+ * of values: {@code String}, {@code Boolean} or {@code Long}.
+ *
+ * @since 0.5
+ */
+@Immutable
+public abstract class AttributeValue {
+  /**
+   * Returns an {@code AttributeValue} with a string value.
+   *
+   * @param stringValue The new value.
+   * @return an {@code AttributeValue} with a string value.
+   * @throws NullPointerException if {@code stringValue} is {@code null}.
+   * @since 0.5
+   */
+  public static AttributeValue stringAttributeValue(String stringValue) {
+    return AttributeValueString.create(stringValue);
+  }
+
+  /**
+   * Returns an {@code AttributeValue} with a boolean value.
+   *
+   * @param booleanValue The new value.
+   * @return an {@code AttributeValue} with a boolean value.
+   * @since 0.5
+   */
+  public static AttributeValue booleanAttributeValue(boolean booleanValue) {
+    return AttributeValueBoolean.create(booleanValue);
+  }
+
+  /**
+   * Returns an {@code AttributeValue} with a long value.
+   *
+   * @param longValue The new value.
+   * @return an {@code AttributeValue} with a long value.
+   * @since 0.5
+   */
+  public static AttributeValue longAttributeValue(long longValue) {
+    return AttributeValueLong.create(longValue);
+  }
+
+  /**
+   * Returns an {@code AttributeValue} with a double value.
+   *
+   * @param doubleValue The new value.
+   * @return an {@code AttributeValue} with a double value.
+   * @since 0.17
+   */
+  public static AttributeValue doubleAttributeValue(double doubleValue) {
+    return AttributeValueDouble.create(doubleValue);
+  }
+
+  AttributeValue() {}
+
+  /**
+   * Applies a function to the underlying value. The function that is called depends on the value's
+   * type, which can be {@code String}, {@code Long}, or {@code Boolean}.
+   *
+   * @param stringFunction the function that should be applied if the value has type {@code String}.
+   * @param longFunction the function that should be applied if the value has type {@code Long}.
+   * @param booleanFunction the function that should be applied if the value has type {@code
+   *     Boolean}.
+   * @param defaultFunction the function that should be applied if the value has a type that was
+   *     added after this {@code match} method was added to the API. See {@link
+   *     io.opencensus.common.Functions} for some common functions for handling unknown types.
+   * @return the result of the function applied to the underlying value.
+   * @since 0.5
+   * @deprecated in favor of {@link #match(Function, Function, Function, Function, Function)}.
+   */
+  @Deprecated
+  public abstract <T> T match(
+      Function<? super String, T> stringFunction,
+      Function<? super Boolean, T> booleanFunction,
+      Function<? super Long, T> longFunction,
+      Function<Object, T> defaultFunction);
+
+  /**
+   * Applies a function to the underlying value. The function that is called depends on the value's
+   * type, which can be {@code String}, {@code Long}, or {@code Boolean}.
+   *
+   * @param stringFunction the function that should be applied if the value has type {@code String}.
+   * @param longFunction the function that should be applied if the value has type {@code Long}.
+   * @param booleanFunction the function that should be applied if the value has type {@code
+   *     Boolean}.
+   * @param doubleFunction the function that should be applied if the value has type {@code Double}.
+   * @param defaultFunction the function that should be applied if the value has a type that was
+   *     added after this {@code match} method was added to the API. See {@link
+   *     io.opencensus.common.Functions} for some common functions for handling unknown types.
+   * @return the result of the function applied to the underlying value.
+   * @since 0.17
+   */
+  @SuppressWarnings("InconsistentOverloads")
+  public abstract <T> T match(
+      Function<? super String, T> stringFunction,
+      Function<? super Boolean, T> booleanFunction,
+      Function<? super Long, T> longFunction,
+      Function<? super Double, T> doubleFunction,
+      Function<Object, T> defaultFunction);
+
+  @Immutable
+  @AutoValue
+  abstract static class AttributeValueString extends AttributeValue {
+
+    AttributeValueString() {}
+
+    static AttributeValue create(String stringValue) {
+      return new AutoValue_AttributeValue_AttributeValueString(
+          Utils.checkNotNull(stringValue, "stringValue"));
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super String, T> stringFunction,
+        Function<? super Boolean, T> booleanFunction,
+        Function<? super Long, T> longFunction,
+        Function<Object, T> defaultFunction) {
+      return stringFunction.apply(getStringValue());
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super String, T> stringFunction,
+        Function<? super Boolean, T> booleanFunction,
+        Function<? super Long, T> longFunction,
+        Function<? super Double, T> doubleFunction,
+        Function<Object, T> defaultFunction) {
+      return stringFunction.apply(getStringValue());
+    }
+
+    abstract String getStringValue();
+  }
+
+  @Immutable
+  @AutoValue
+  abstract static class AttributeValueBoolean extends AttributeValue {
+
+    AttributeValueBoolean() {}
+
+    static AttributeValue create(Boolean booleanValue) {
+      return new AutoValue_AttributeValue_AttributeValueBoolean(
+          Utils.checkNotNull(booleanValue, "booleanValue"));
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super String, T> stringFunction,
+        Function<? super Boolean, T> booleanFunction,
+        Function<? super Long, T> longFunction,
+        Function<Object, T> defaultFunction) {
+      return booleanFunction.apply(getBooleanValue());
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super String, T> stringFunction,
+        Function<? super Boolean, T> booleanFunction,
+        Function<? super Long, T> longFunction,
+        Function<? super Double, T> doubleFunction,
+        Function<Object, T> defaultFunction) {
+      return booleanFunction.apply(getBooleanValue());
+    }
+
+    abstract Boolean getBooleanValue();
+  }
+
+  @Immutable
+  @AutoValue
+  abstract static class AttributeValueLong extends AttributeValue {
+
+    AttributeValueLong() {}
+
+    static AttributeValue create(Long longValue) {
+      return new AutoValue_AttributeValue_AttributeValueLong(
+          Utils.checkNotNull(longValue, "longValue"));
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super String, T> stringFunction,
+        Function<? super Boolean, T> booleanFunction,
+        Function<? super Long, T> longFunction,
+        Function<Object, T> defaultFunction) {
+      return longFunction.apply(getLongValue());
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super String, T> stringFunction,
+        Function<? super Boolean, T> booleanFunction,
+        Function<? super Long, T> longFunction,
+        Function<? super Double, T> doubleFunction,
+        Function<Object, T> defaultFunction) {
+      return longFunction.apply(getLongValue());
+    }
+
+    abstract Long getLongValue();
+  }
+
+  @Immutable
+  @AutoValue
+  abstract static class AttributeValueDouble extends AttributeValue {
+
+    AttributeValueDouble() {}
+
+    static AttributeValue create(Double doubleValue) {
+      return new AutoValue_AttributeValue_AttributeValueDouble(
+          Utils.checkNotNull(doubleValue, "doubleValue"));
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super String, T> stringFunction,
+        Function<? super Boolean, T> booleanFunction,
+        Function<? super Long, T> longFunction,
+        Function<Object, T> defaultFunction) {
+      return defaultFunction.apply(getDoubleValue());
+    }
+
+    @Override
+    public final <T> T match(
+        Function<? super String, T> stringFunction,
+        Function<? super Boolean, T> booleanFunction,
+        Function<? super Long, T> longFunction,
+        Function<? super Double, T> doubleFunction,
+        Function<Object, T> defaultFunction) {
+      return doubleFunction.apply(getDoubleValue());
+    }
+
+    abstract Double getDoubleValue();
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/BaseMessageEvent.java b/api/src/main/java/io/opencensus/trace/BaseMessageEvent.java
new file mode 100644
index 0000000..5ad961f
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/BaseMessageEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018, 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.trace;
+
+/**
+ * Superclass for {@link MessageEvent} and {@link NetworkEvent} to resolve API backward
+ * compatibility issue.
+ *
+ * <p>{@code SpanData.create} can't be overloaded with parameter types that differ only in the type
+ * of the TimedEvent, because the signatures are the same after generic type erasure. {@code
+ * BaseMessageEvent} allows the same method to accept both {@code TimedEvents<NetworkEvent>} and
+ * {@code TimedEvents<MessageEvent>}.
+ *
+ * <p>This class should only be extended by {@code NetworkEvent} and {@code MessageEvent}.
+ *
+ * @deprecated This class is for internal use only.
+ * @since 0.12
+ */
+@Deprecated
+public abstract class BaseMessageEvent {
+  // package protected to avoid users to extend it.
+  BaseMessageEvent() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/BlankSpan.java b/api/src/main/java/io/opencensus/trace/BlankSpan.java
new file mode 100644
index 0000000..af6456d
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/BlankSpan.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The {@code BlankSpan} is a singleton class, which is the default {@link Span} that is used when
+ * no {@code Span} implementation is available. All operations are no-op.
+ *
+ * <p>Used also to stop tracing, see {@link Tracer#withSpan}.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class BlankSpan extends Span {
+  /**
+   * Singleton instance of this class.
+   *
+   * @since 0.5
+   */
+  public static final BlankSpan INSTANCE = new BlankSpan();
+
+  private BlankSpan() {
+    super(SpanContext.INVALID, null);
+  }
+
+  /** No-op implementation of the {@link Span#putAttribute(String, AttributeValue)} method. */
+  @Override
+  public void putAttribute(String key, AttributeValue value) {
+    Utils.checkNotNull(key, "key");
+    Utils.checkNotNull(value, "value");
+  }
+
+  /** No-op implementation of the {@link Span#putAttributes(Map)} method. */
+  @Override
+  public void putAttributes(Map<String, AttributeValue> attributes) {
+    Utils.checkNotNull(attributes, "attributes");
+  }
+
+  /** No-op implementation of the {@link Span#addAnnotation(String, Map)} method. */
+  @Override
+  public void addAnnotation(String description, Map<String, AttributeValue> attributes) {
+    Utils.checkNotNull(description, "description");
+    Utils.checkNotNull(attributes, "attributes");
+  }
+
+  /** No-op implementation of the {@link Span#addAnnotation(Annotation)} method. */
+  @Override
+  public void addAnnotation(Annotation annotation) {
+    Utils.checkNotNull(annotation, "annotation");
+  }
+
+  /** No-op implementation of the {@link Span#addNetworkEvent(NetworkEvent)} method. */
+  @Override
+  @Deprecated
+  public void addNetworkEvent(NetworkEvent networkEvent) {}
+
+  /** No-op implementation of the {@link Span#addMessageEvent(MessageEvent)} method. */
+  @Override
+  public void addMessageEvent(MessageEvent messageEvent) {
+    Utils.checkNotNull(messageEvent, "messageEvent");
+  }
+
+  /** No-op implementation of the {@link Span#addLink(Link)} method. */
+  @Override
+  public void addLink(Link link) {
+    Utils.checkNotNull(link, "link");
+  }
+
+  @Override
+  public void setStatus(Status status) {
+    Utils.checkNotNull(status, "status");
+  }
+
+  /** No-op implementation of the {@link Span#end(EndSpanOptions)} method. */
+  @Override
+  public void end(EndSpanOptions options) {
+    Utils.checkNotNull(options, "options");
+  }
+
+  @Override
+  public String toString() {
+    return "BlankSpan";
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/CurrentSpanUtils.java b/api/src/main/java/io/opencensus/trace/CurrentSpanUtils.java
new file mode 100644
index 0000000..aa2f055
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/CurrentSpanUtils.java
@@ -0,0 +1,180 @@
+/*
+ * 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.trace;
+
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.trace.unsafe.ContextUtils;
+import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
+
+/** Util methods/functionality to interact with the {@link Span} in the {@link io.grpc.Context}. */
+final class CurrentSpanUtils {
+  // No instance of this class.
+  private CurrentSpanUtils() {}
+
+  /**
+   * Returns The {@link Span} from the current context.
+   *
+   * @return The {@code Span} from the current context.
+   */
+  @Nullable
+  static Span getCurrentSpan() {
+    return ContextUtils.CONTEXT_SPAN_KEY.get();
+  }
+
+  /**
+   * Enters the scope of code where the given {@link Span} is in the current context, and returns an
+   * object that represents that scope. The scope is exited when the returned object is closed.
+   *
+   * <p>Supports try-with-resource idiom.
+   *
+   * @param span The {@code Span} to be set to the current context.
+   * @param endSpan if {@code true} the returned {@code Scope} will close the {@code Span}.
+   * @return An object that defines a scope where the given {@code Span} is set to the current
+   *     context.
+   */
+  static Scope withSpan(Span span, boolean endSpan) {
+    return new ScopeInSpan(span, endSpan);
+  }
+
+  /**
+   * Wraps a {@link Runnable} so that it executes with the {@code span} as the current {@code Span}.
+   *
+   * @param span the {@code Span} to be set as current.
+   * @param endSpan if {@code true} the returned {@code Runnable} will close the {@code Span}.
+   * @param runnable the {@code Runnable} to run in the {@code Span}.
+   * @return the wrapped {@code Runnable}.
+   */
+  static Runnable withSpan(Span span, boolean endSpan, Runnable runnable) {
+    return new RunnableInSpan(span, runnable, endSpan);
+  }
+
+  /**
+   * Wraps a {@link Callable} so that it executes with the {@code span} as the current {@code Span}.
+   *
+   * @param span the {@code Span} to be set as current.
+   * @param endSpan if {@code true} the returned {@code Runnable} will close the {@code Span}.
+   * @param callable the {@code Callable} to run in the {@code Span}.
+   * @return the wrapped {@code Callable}.
+   */
+  static <C> Callable<C> withSpan(Span span, boolean endSpan, Callable<C> callable) {
+    return new CallableInSpan<C>(span, callable, endSpan);
+  }
+
+  // Defines an arbitrary scope of code as a traceable operation. Supports try-with-resources idiom.
+  private static final class ScopeInSpan implements Scope {
+    private final Context origContext;
+    private final Span span;
+    private final boolean endSpan;
+
+    /**
+     * Constructs a new {@link ScopeInSpan}.
+     *
+     * @param span is the {@code Span} to be added to the current {@code io.grpc.Context}.
+     */
+    private ScopeInSpan(Span span, boolean endSpan) {
+      this.span = span;
+      this.endSpan = endSpan;
+      origContext = Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, span).attach();
+    }
+
+    @Override
+    public void close() {
+      Context.current().detach(origContext);
+      if (endSpan) {
+        span.end();
+      }
+    }
+  }
+
+  private static final class RunnableInSpan implements Runnable {
+    // TODO(bdrutu): Investigate if the extra private visibility increases the generated bytecode.
+    private final Span span;
+    private final Runnable runnable;
+    private final boolean endSpan;
+
+    private RunnableInSpan(Span span, Runnable runnable, boolean endSpan) {
+      this.span = span;
+      this.runnable = runnable;
+      this.endSpan = endSpan;
+    }
+
+    @Override
+    public void run() {
+      Context origContext =
+          Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, span).attach();
+      try {
+        runnable.run();
+      } catch (Throwable t) {
+        setErrorStatus(span, t);
+        if (t instanceof RuntimeException) {
+          throw (RuntimeException) t;
+        } else if (t instanceof Error) {
+          throw (Error) t;
+        }
+        throw new RuntimeException("unexpected", t);
+      } finally {
+        Context.current().detach(origContext);
+        if (endSpan) {
+          span.end();
+        }
+      }
+    }
+  }
+
+  private static final class CallableInSpan<V> implements Callable<V> {
+    private final Span span;
+    private final Callable<V> callable;
+    private final boolean endSpan;
+
+    private CallableInSpan(Span span, Callable<V> callable, boolean endSpan) {
+      this.span = span;
+      this.callable = callable;
+      this.endSpan = endSpan;
+    }
+
+    @Override
+    public V call() throws Exception {
+      Context origContext =
+          Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, span).attach();
+      try {
+        return callable.call();
+      } catch (Exception e) {
+        setErrorStatus(span, e);
+        throw e;
+      } catch (Throwable t) {
+        setErrorStatus(span, t);
+        if (t instanceof Error) {
+          throw (Error) t;
+        }
+        throw new RuntimeException("unexpected", t);
+      } finally {
+        Context.current().detach(origContext);
+        if (endSpan) {
+          span.end();
+        }
+      }
+    }
+  }
+
+  private static void setErrorStatus(Span span, Throwable t) {
+    span.setStatus(
+        Status.UNKNOWN.withDescription(
+            t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage()));
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/EndSpanOptions.java b/api/src/main/java/io/opencensus/trace/EndSpanOptions.java
new file mode 100644
index 0000000..b0d9a47
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/EndSpanOptions.java
@@ -0,0 +1,127 @@
+/*
+ * 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import java.util.Collection;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that enables overriding the default values used when ending a {@link Span}. Allows
+ * overriding the {@link Status status}.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class EndSpanOptions {
+  /**
+   * The default {@code EndSpanOptions}.
+   *
+   * @since 0.5
+   */
+  public static final EndSpanOptions DEFAULT = builder().build();
+
+  /**
+   * Returns a new {@link Builder} with default options.
+   *
+   * @return a new {@code Builder} with default options.
+   * @since 0.5
+   */
+  public static Builder builder() {
+    return new AutoValue_EndSpanOptions.Builder().setSampleToLocalSpanStore(false);
+  }
+
+  /**
+   * If {@code true} this is equivalent with calling the {@link
+   * io.opencensus.trace.export.SampledSpanStore#registerSpanNamesForCollection(Collection)} in
+   * advance for this span name.
+   *
+   * <p>It is strongly recommended to use the {@link
+   * io.opencensus.trace.export.SampledSpanStore#registerSpanNamesForCollection(Collection)} API
+   * instead.
+   *
+   * @return {@code true} if the name of the {@code Span} should be registered to the {@code
+   *     io.opencensus.trace.export.SampledSpanStore}.
+   * @since 0.8
+   */
+  @ExperimentalApi
+  public abstract boolean getSampleToLocalSpanStore();
+
+  /**
+   * Returns the status.
+   *
+   * <p>If {@code null} then the {@link Span} will record the {@link Status} set via {@link
+   * Span#setStatus(Status)} or the default {@link Status#OK} if no status was set.
+   *
+   * @return the status.
+   * @since 0.5
+   */
+  @Nullable
+  public abstract Status getStatus();
+
+  /**
+   * Builder class for {@link EndSpanOptions}.
+   *
+   * @since 0.5
+   */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /**
+     * Sets the status for the {@link Span}.
+     *
+     * <p>If set, this will override the status set via {@link Span#setStatus(Status)}.
+     *
+     * @param status the status.
+     * @return this.
+     * @since 0.5
+     */
+    public abstract Builder setStatus(Status status);
+
+    /**
+     * If set to {@code true} this is equivalent with calling the {@link
+     * io.opencensus.trace.export.SampledSpanStore#registerSpanNamesForCollection(Collection)} in
+     * advance for the given span name.
+     *
+     * <p>WARNING: setting this option to a randomly generated span name can OOM your process
+     * because the library will save samples for each name.
+     *
+     * <p>It is strongly recommended to use the {@link
+     * io.opencensus.trace.export.SampledSpanStore#registerSpanNamesForCollection(Collection)} API
+     * instead.
+     *
+     * @return this.
+     * @since 0.8
+     */
+    @ExperimentalApi
+    public abstract Builder setSampleToLocalSpanStore(boolean sampleToLocalSpanStore);
+
+    /**
+     * Builds and returns a {@code EndSpanOptions} with the desired settings.
+     *
+     * @return a {@code EndSpanOptions} with the desired settings.
+     * @since 0.5
+     */
+    public abstract EndSpanOptions build();
+
+    Builder() {}
+  }
+
+  EndSpanOptions() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/Link.java b/api/src/main/java/io/opencensus/trace/Link.java
new file mode 100644
index 0000000..1de7971
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Link.java
@@ -0,0 +1,124 @@
+/*
+ * 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.trace;
+
+import com.google.auto.value.AutoValue;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A link to a {@link Span} from a different trace.
+ *
+ * <p>It requires a {@link Type} which describes the relationship with the linked {@code Span} and
+ * the identifiers of the linked {@code Span}.
+ *
+ * <p>Used (for example) in batching operations, where a single batch handler processes multiple
+ * requests from different traces.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class Link {
+  private static final Map<String, AttributeValue> EMPTY_ATTRIBUTES = Collections.emptyMap();
+
+  /**
+   * The relationship with the linked {@code Span} relative to the current {@code Span}.
+   *
+   * @since 0.5
+   */
+  public enum Type {
+    /**
+     * When the linked {@code Span} is a child of the current {@code Span}.
+     *
+     * @since 0.5
+     */
+    CHILD_LINKED_SPAN,
+    /**
+     * When the linked {@code Span} is a parent of the current {@code Span}.
+     *
+     * @since 0.5
+     */
+    PARENT_LINKED_SPAN
+  }
+
+  /**
+   * Returns a new {@code Link}.
+   *
+   * @param context the context of the linked {@code Span}.
+   * @param type the type of the relationship with the linked {@code Span}.
+   * @return a new {@code Link}.
+   * @since 0.5
+   */
+  public static Link fromSpanContext(SpanContext context, Type type) {
+    return new AutoValue_Link(context.getTraceId(), context.getSpanId(), type, EMPTY_ATTRIBUTES);
+  }
+
+  /**
+   * Returns a new {@code Link}.
+   *
+   * @param context the context of the linked {@code Span}.
+   * @param type the type of the relationship with the linked {@code Span}.
+   * @param attributes the attributes of the {@code Link}.
+   * @return a new {@code Link}.
+   * @since 0.5
+   */
+  public static Link fromSpanContext(
+      SpanContext context, Type type, Map<String, AttributeValue> attributes) {
+    return new AutoValue_Link(
+        context.getTraceId(),
+        context.getSpanId(),
+        type,
+        Collections.unmodifiableMap(new HashMap<String, AttributeValue>(attributes)));
+  }
+
+  /**
+   * Returns the {@code TraceId}.
+   *
+   * @return the {@code TraceId}.
+   * @since 0.5
+   */
+  public abstract TraceId getTraceId();
+
+  /**
+   * Returns the {@code SpanId}.
+   *
+   * @return the {@code SpanId}
+   * @since 0.5
+   */
+  public abstract SpanId getSpanId();
+
+  /**
+   * Returns the {@code Type}.
+   *
+   * @return the {@code Type}.
+   * @since 0.5
+   */
+  public abstract Type getType();
+
+  /**
+   * Returns the set of attributes.
+   *
+   * @return the set of attributes.
+   * @since 0.5
+   */
+  public abstract Map<String, AttributeValue> getAttributes();
+
+  Link() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/LowerCaseBase16Encoding.java b/api/src/main/java/io/opencensus/trace/LowerCaseBase16Encoding.java
new file mode 100644
index 0000000..bca9586
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/LowerCaseBase16Encoding.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.Arrays;
+
+/** Internal copy of the Guava implementation of the {@code BaseEncoding.base16().lowerCase()}. */
+final class LowerCaseBase16Encoding {
+  private static final String ALPHABET = "0123456789abcdef";
+  private static final int ASCII_CHARACTERS = 128;
+  private static final char[] ENCODING = buildEncodingArray();
+  private static final byte[] DECODING = buildDecodingArray();
+
+  private static char[] buildEncodingArray() {
+    char[] encoding = new char[512];
+    for (int i = 0; i < 256; ++i) {
+      encoding[i] = ALPHABET.charAt(i >>> 4);
+      encoding[i | 0x100] = ALPHABET.charAt(i & 0xF);
+    }
+    return encoding;
+  }
+
+  private static byte[] buildDecodingArray() {
+    byte[] decoding = new byte[ASCII_CHARACTERS];
+    Arrays.fill(decoding, (byte) -1);
+    for (int i = 0; i < ALPHABET.length(); i++) {
+      char c = ALPHABET.charAt(i);
+      decoding[c] = (byte) i;
+    }
+    return decoding;
+  }
+
+  /**
+   * Encodes the specified byte array, and returns the encoded {@code String}.
+   *
+   * @param bytes byte array to be encoded.
+   * @return the encoded {@code String}.
+   */
+  static String encodeToString(byte[] bytes) {
+    StringBuilder stringBuilder = new StringBuilder(bytes.length * 2);
+    for (byte byteVal : bytes) {
+      int b = byteVal & 0xFF;
+      stringBuilder.append(ENCODING[b]);
+      stringBuilder.append(ENCODING[b | 0x100]);
+    }
+    return stringBuilder.toString();
+  }
+
+  /**
+   * Decodes the specified character sequence, and returns the resulting {@code byte[]}.
+   *
+   * @param chars the character sequence to be decoded.
+   * @return the resulting {@code byte[]}
+   * @throws IllegalArgumentException if the input is not a valid encoded string according to this
+   *     encoding.
+   */
+  static byte[] decodeToBytes(CharSequence chars) {
+    Utils.checkArgument(chars.length() % 2 == 0, "Invalid input length " + chars.length());
+    int bytesWritten = 0;
+    byte[] bytes = new byte[chars.length() / 2];
+    for (int i = 0; i < chars.length(); i += 2) {
+      bytes[bytesWritten++] = decodeByte(chars.charAt(i), chars.charAt(i + 1));
+    }
+    return bytes;
+  }
+
+  private static byte decodeByte(char hi, char lo) {
+    Utils.checkArgument(lo < ASCII_CHARACTERS && DECODING[lo] != -1, "Invalid character " + lo);
+    Utils.checkArgument(hi < ASCII_CHARACTERS && DECODING[hi] != -1, "Invalid character " + hi);
+    int decoded = DECODING[hi] << 4 | DECODING[lo];
+    return (byte) decoded;
+  }
+
+  // Private constructor to disallow instances.
+  private LowerCaseBase16Encoding() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/MessageEvent.java b/api/src/main/java/io/opencensus/trace/MessageEvent.java
new file mode 100644
index 0000000..4b693aa
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/MessageEvent.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a generic messaging event. This class can represent messaging happened in
+ * any layer, especially higher application layer. Thus, it can be used when recording events in
+ * pipeline works, in-process bidirectional streams and batch processing.
+ *
+ * <p>It requires a {@link Type type} and a message id that serves to uniquely identify each
+ * message. It can optionally have information about the message size.
+ *
+ * @since 0.12
+ */
+@Immutable
+@AutoValue
+@SuppressWarnings("deprecation")
+public abstract class MessageEvent extends BaseMessageEvent {
+  /**
+   * Available types for a {@code MessageEvent}.
+   *
+   * @since 0.12
+   */
+  public enum Type {
+    /**
+     * When the message was sent.
+     *
+     * @since 0.12
+     */
+    SENT,
+    /**
+     * When the message was received.
+     *
+     * @since 0.12
+     */
+    RECEIVED,
+  }
+
+  /**
+   * Returns a new {@link Builder} with default values.
+   *
+   * @param type designates whether this is a send or receive message.
+   * @param messageId serves to uniquely identify each message.
+   * @return a new {@code Builder} with default values.
+   * @throws NullPointerException if {@code type} is {@code null}.
+   * @since 0.12
+   */
+  public static Builder builder(Type type, long messageId) {
+    return new AutoValue_MessageEvent.Builder()
+        .setType(Utils.checkNotNull(type, "type"))
+        .setMessageId(messageId)
+        // We need to set a value for the message size because the autovalue requires all
+        // primitives to be initialized.
+        .setUncompressedMessageSize(0)
+        .setCompressedMessageSize(0);
+  }
+
+  /**
+   * Returns the type of the {@code MessageEvent}.
+   *
+   * @return the type of the {@code MessageEvent}.
+   * @since 0.12
+   */
+  public abstract Type getType();
+
+  /**
+   * Returns the message id argument that serves to uniquely identify each message.
+   *
+   * @return the message id of the {@code MessageEvent}.
+   * @since 0.12
+   */
+  public abstract long getMessageId();
+
+  /**
+   * Returns the uncompressed size in bytes of the {@code MessageEvent}.
+   *
+   * @return the uncompressed size in bytes of the {@code MessageEvent}.
+   * @since 0.12
+   */
+  public abstract long getUncompressedMessageSize();
+
+  /**
+   * Returns the compressed size in bytes of the {@code MessageEvent}.
+   *
+   * @return the compressed size in bytes of the {@code MessageEvent}.
+   * @since 0.12
+   */
+  public abstract long getCompressedMessageSize();
+
+  /**
+   * Builder class for {@link MessageEvent}.
+   *
+   * @since 0.12
+   */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    // Package protected methods because these values are mandatory and set only in the
+    // MessageEvent#builder() function.
+    abstract Builder setType(Type type);
+
+    abstract Builder setMessageId(long messageId);
+
+    /**
+     * Sets the uncompressed message size.
+     *
+     * @param uncompressedMessageSize represents the uncompressed size in bytes of this message.
+     * @return this.
+     * @since 0.12
+     */
+    public abstract Builder setUncompressedMessageSize(long uncompressedMessageSize);
+
+    /**
+     * Sets the compressed message size.
+     *
+     * @param compressedMessageSize represents the compressed size in bytes of this message.
+     * @return this.
+     * @since 0.12
+     */
+    public abstract Builder setCompressedMessageSize(long compressedMessageSize);
+
+    /**
+     * Builds and returns a {@code MessageEvent} with the desired values.
+     *
+     * @return a {@code MessageEvent} with the desired values.
+     * @since 0.12
+     */
+    public abstract MessageEvent build();
+
+    Builder() {}
+  }
+
+  MessageEvent() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/NetworkEvent.java b/api/src/main/java/io/opencensus/trace/NetworkEvent.java
new file mode 100644
index 0000000..722029e
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/NetworkEvent.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a network event. It requires a {@link Type type} and a message id that
+ * serves to uniquely identify each network message. It can optionally can have information about
+ * the kernel time and message size.
+ *
+ * @deprecated Use {@link MessageEvent}.
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+@AutoValue.CopyAnnotations
+@Deprecated
+public abstract class NetworkEvent extends io.opencensus.trace.BaseMessageEvent {
+  /**
+   * Available types for a {@code NetworkEvent}.
+   *
+   * @since 0.5
+   */
+  public enum Type {
+    /**
+     * When the message was sent.
+     *
+     * @since 0.5
+     */
+    SENT,
+    /**
+     * When the message was received.
+     *
+     * @since 0.5
+     */
+    RECV,
+  }
+
+  /**
+   * Returns a new {@link Builder} with default values.
+   *
+   * @param type designates whether this is a network send or receive message.
+   * @param messageId serves to uniquely identify each network message.
+   * @return a new {@code Builder} with default values.
+   * @throws NullPointerException if {@code type} is {@code null}.
+   * @since 0.5
+   */
+  public static Builder builder(Type type, long messageId) {
+    return new AutoValue_NetworkEvent.Builder()
+        .setType(Utils.checkNotNull(type, "type"))
+        .setMessageId(messageId)
+        // We need to set a value for the message size because the autovalue requires all
+        // primitives to be initialized.
+        .setUncompressedMessageSize(0)
+        .setCompressedMessageSize(0);
+  }
+
+  /**
+   * Returns the kernel timestamp associated with the {@code NetworkEvent} or {@code null} if not
+   * set.
+   *
+   * @return the kernel timestamp associated with the {@code NetworkEvent} or {@code null} if not
+   *     set.
+   * @since 0.5
+   */
+  @Nullable
+  public abstract Timestamp getKernelTimestamp();
+
+  /**
+   * Returns the type of the {@code NetworkEvent}.
+   *
+   * @return the type of the {@code NetworkEvent}.
+   * @since 0.5
+   */
+  public abstract Type getType();
+
+  /**
+   * Returns the message id argument that serves to uniquely identify each network message.
+   *
+   * @return the message id of the {@code NetworkEvent}.
+   * @since 0.5
+   */
+  public abstract long getMessageId();
+
+  /**
+   * Returns the uncompressed size in bytes of the {@code NetworkEvent}.
+   *
+   * @return the uncompressed size in bytes of the {@code NetworkEvent}.
+   * @since 0.6
+   */
+  public abstract long getUncompressedMessageSize();
+
+  /**
+   * Returns the compressed size in bytes of the {@code NetworkEvent}.
+   *
+   * @return the compressed size in bytes of the {@code NetworkEvent}.
+   * @since 0.6
+   */
+  public abstract long getCompressedMessageSize();
+
+  /**
+   * Returns the uncompressed size in bytes of the {@code NetworkEvent}.
+   *
+   * @deprecated Use {@link #getUncompressedMessageSize}.
+   * @return the uncompressed size in bytes of the {@code NetworkEvent}.
+   * @since 0.5
+   */
+  @Deprecated
+  public long getMessageSize() {
+    return getUncompressedMessageSize();
+  }
+
+  /**
+   * Builder class for {@link NetworkEvent}.
+   *
+   * @deprecated {@link NetworkEvent} is deprecated. Please use {@link MessageEvent} and its builder
+   *     {@link MessageEvent.Builder}.
+   * @since 0.5
+   */
+  @AutoValue.Builder
+  @Deprecated
+  public abstract static class Builder {
+    // Package protected methods because these values are mandatory and set only in the
+    // NetworkEvent#builder() function.
+    abstract Builder setType(Type type);
+
+    abstract Builder setMessageId(long messageId);
+
+    /**
+     * Sets the kernel timestamp.
+     *
+     * @param kernelTimestamp The kernel timestamp of the event.
+     * @return this.
+     * @since 0.5
+     */
+    public abstract Builder setKernelTimestamp(@Nullable Timestamp kernelTimestamp);
+
+    /**
+     * Sets the uncompressed message size.
+     *
+     * @deprecated Use {@link #setUncompressedMessageSize}.
+     * @param messageSize represents the uncompressed size in bytes of this message.
+     * @return this.
+     * @since 0.5
+     */
+    @Deprecated
+    public Builder setMessageSize(long messageSize) {
+      return setUncompressedMessageSize(messageSize);
+    }
+
+    /**
+     * Sets the uncompressed message size.
+     *
+     * @param uncompressedMessageSize represents the uncompressed size in bytes of this message.
+     * @return this.
+     * @since 0.6
+     */
+    public abstract Builder setUncompressedMessageSize(long uncompressedMessageSize);
+
+    /**
+     * Sets the compressed message size.
+     *
+     * @param compressedMessageSize represents the compressed size in bytes of this message.
+     * @return this.
+     * @since 0.6
+     */
+    public abstract Builder setCompressedMessageSize(long compressedMessageSize);
+
+    /**
+     * Builds and returns a {@code NetworkEvent} with the desired values.
+     *
+     * @return a {@code NetworkEvent} with the desired values.
+     * @since 0.5
+     */
+    public abstract NetworkEvent build();
+
+    Builder() {}
+  }
+
+  NetworkEvent() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/Sampler.java b/api/src/main/java/io/opencensus/trace/Sampler.java
new file mode 100644
index 0000000..e89af89
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Sampler.java
@@ -0,0 +1,61 @@
+/*
+ * 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.trace;
+
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Sampler is used to make decisions on {@link Span} sampling.
+ *
+ * @since 0.5
+ */
+public abstract class Sampler {
+  /**
+   * Called during {@link Span} creation to make a sampling decision.
+   *
+   * @param parentContext the parent span's {@link SpanContext}. {@code null} if this is a root
+   *     span.
+   * @param hasRemoteParent {@code true} if the parent {@code Span} is remote. {@code null} if this
+   *     is a root span.
+   * @param traceId the {@link TraceId} for the new {@code Span}. This will be identical to that in
+   *     the parentContext, unless this is a root span.
+   * @param spanId the {@link SpanId} for the new {@code Span}.
+   * @param name the name of the new {@code Span}.
+   * @param parentLinks the parentLinks associated with the new {@code Span}.
+   * @return {@code true} if the {@code Span} is sampled.
+   * @since 0.5
+   */
+  public abstract boolean shouldSample(
+      @Nullable SpanContext parentContext,
+      @Nullable Boolean hasRemoteParent,
+      TraceId traceId,
+      SpanId spanId,
+      String name,
+      List<Span> parentLinks);
+
+  /**
+   * Returns the description of this {@code Sampler}. This may be displayed on debug pages or in the
+   * logs.
+   *
+   * <p>Example: "ProbabilitySampler{0.000100}"
+   *
+   * @return the description of this {@code Sampler}.
+   * @since 0.6
+   */
+  public abstract String getDescription();
+}
diff --git a/api/src/main/java/io/opencensus/trace/Span.java b/api/src/main/java/io/opencensus/trace/Span.java
new file mode 100644
index 0000000..8f8253b
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Span.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.internal.BaseMessageEventUtils;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/**
+ * An abstract class that represents a span. It has an associated {@link SpanContext} and a set of
+ * {@link Options}.
+ *
+ * <p>Spans are created by the {@link SpanBuilder#startSpan} method.
+ *
+ * <p>{@code Span} <b>must</b> be ended by calling {@link #end()} or {@link #end(EndSpanOptions)}
+ *
+ * @since 0.5
+ */
+public abstract class Span {
+  private static final Map<String, AttributeValue> EMPTY_ATTRIBUTES = Collections.emptyMap();
+
+  // Contains the identifiers associated with this Span.
+  private final SpanContext context;
+
+  // Contains the options associated with this Span. This object is immutable.
+  private final Set<Options> options;
+
+  /**
+   * {@code Span} options. These options are NOT propagated to child spans. These options determine
+   * features such as whether a {@code Span} should record any annotations or events.
+   *
+   * @since 0.5
+   */
+  public enum Options {
+    /**
+     * This option is set if the Span is part of a sampled distributed trace OR {@link
+     * SpanBuilder#setRecordEvents(boolean)} was called with true.
+     *
+     * @since 0.5
+     */
+    RECORD_EVENTS;
+  }
+
+  private static final Set<Options> DEFAULT_OPTIONS =
+      Collections.unmodifiableSet(EnumSet.noneOf(Options.class));
+
+  /**
+   * Creates a new {@code Span}.
+   *
+   * @param context the context associated with this {@code Span}.
+   * @param options the options associated with this {@code Span}. If {@code null} then default
+   *     options will be set.
+   * @throws NullPointerException if context is {@code null}.
+   * @throws IllegalArgumentException if the {@code SpanContext} is sampled but no RECORD_EVENTS
+   *     options.
+   * @since 0.5
+   */
+  protected Span(SpanContext context, @Nullable EnumSet<Options> options) {
+    this.context = Utils.checkNotNull(context, "context");
+    this.options =
+        options == null
+            ? DEFAULT_OPTIONS
+            : Collections.<Options>unmodifiableSet(EnumSet.copyOf(options));
+    Utils.checkArgument(
+        !context.getTraceOptions().isSampled() || (this.options.contains(Options.RECORD_EVENTS)),
+        "Span is sampled, but does not have RECORD_EVENTS set.");
+  }
+
+  /**
+   * Sets an attribute to the {@code Span}. If the {@code Span} previously contained a mapping for
+   * the key, the old value is replaced by the specified value.
+   *
+   * @param key the key for this attribute.
+   * @param value the value for this attribute.
+   * @since 0.6
+   */
+  public void putAttribute(String key, AttributeValue value) {
+    // Not final because for performance reasons we want to override this in the implementation.
+    // Also a default implementation is needed to not break the compatibility (users may extend this
+    // for testing).
+    Utils.checkNotNull(key, "key");
+    Utils.checkNotNull(value, "value");
+    putAttributes(Collections.singletonMap(key, value));
+  }
+
+  /**
+   * Sets a set of attributes to the {@code Span}. The effect of this call is equivalent to that of
+   * calling {@link #putAttribute(String, AttributeValue)} once for each element in the specified
+   * map.
+   *
+   * @param attributes the attributes that will be added and associated with the {@code Span}.
+   * @since 0.6
+   */
+  public void putAttributes(Map<String, AttributeValue> attributes) {
+    // Not final because we want to start overriding this method from the beginning, this will
+    // allow us to remove the addAttributes faster. All implementations MUST override this method.
+    Utils.checkNotNull(attributes, "attributes");
+    addAttributes(attributes);
+  }
+
+  /**
+   * Sets a set of attributes to the {@code Span}. The effect of this call is equivalent to that of
+   * calling {@link #putAttribute(String, AttributeValue)} once for each element in the specified
+   * map.
+   *
+   * @deprecated Use {@link #putAttributes(Map)}
+   * @param attributes the attributes that will be added and associated with the {@code Span}.
+   * @since 0.5
+   */
+  @Deprecated
+  public void addAttributes(Map<String, AttributeValue> attributes) {
+    putAttributes(attributes);
+  }
+
+  /**
+   * Adds an annotation to the {@code Span}.
+   *
+   * @param description the description of the annotation time event.
+   * @since 0.5
+   */
+  public final void addAnnotation(String description) {
+    Utils.checkNotNull(description, "description");
+    addAnnotation(description, EMPTY_ATTRIBUTES);
+  }
+
+  /**
+   * Adds an annotation to the {@code Span}.
+   *
+   * @param description the description of the annotation time event.
+   * @param attributes the attributes that will be added; these are associated with this annotation,
+   *     not the {@code Span} as for {@link #putAttributes(Map)}.
+   * @since 0.5
+   */
+  public abstract void addAnnotation(String description, Map<String, AttributeValue> attributes);
+
+  /**
+   * Adds an annotation to the {@code Span}.
+   *
+   * @param annotation the annotations to add.
+   * @since 0.5
+   */
+  public abstract void addAnnotation(Annotation annotation);
+
+  /**
+   * Adds a NetworkEvent to the {@code Span}.
+   *
+   * <p>This function is only intended to be used by RPC systems (either client or server), not by
+   * higher level applications.
+   *
+   * @param networkEvent the network event to add.
+   * @deprecated Use {@link #addMessageEvent}.
+   * @since 0.5
+   */
+  @Deprecated
+  public void addNetworkEvent(NetworkEvent networkEvent) {
+    addMessageEvent(BaseMessageEventUtils.asMessageEvent(networkEvent));
+  }
+
+  /**
+   * Adds a MessageEvent to the {@code Span}.
+   *
+   * <p>This function can be used by higher level applications to record messaging event.
+   *
+   * <p>This method should always be overridden by users whose API versions are larger or equal to
+   * {@code 0.12}.
+   *
+   * @param messageEvent the message to add.
+   * @since 0.12
+   */
+  public void addMessageEvent(MessageEvent messageEvent) {
+    // Default implementation by invoking addNetworkEvent() so that any existing derived classes,
+    // including implementation and the mocked ones, do not need to override this method explicitly.
+    Utils.checkNotNull(messageEvent, "messageEvent");
+    addNetworkEvent(BaseMessageEventUtils.asNetworkEvent(messageEvent));
+  }
+
+  /**
+   * Adds a {@link Link} to the {@code Span}.
+   *
+   * <p>Used (for example) in batching operations, where a single batch handler processes multiple
+   * requests from different traces.
+   *
+   * @param link the link to add.
+   * @since 0.5
+   */
+  public abstract void addLink(Link link);
+
+  /**
+   * Sets the {@link Status} to the {@code Span}.
+   *
+   * <p>If used, this will override the default {@code Span} status. Default is {@link Status#OK}.
+   *
+   * <p>Only the value of the last call will be recorded, and implementations are free to ignore
+   * previous calls. If the status is set via {@link EndSpanOptions.Builder#setStatus(Status)} that
+   * will always be the last call.
+   *
+   * @param status the {@link Status} to set.
+   * @since 0.9
+   */
+  public void setStatus(Status status) {
+    // Implemented as no-op for backwards compatibility (for example gRPC extends Span in tests).
+    // Implementation must override this method.
+    Utils.checkNotNull(status, "status");
+  }
+
+  /**
+   * Marks the end of {@code Span} execution with the given options.
+   *
+   * <p>Only the timing of the first end call for a given {@code Span} will be recorded, and
+   * implementations are free to ignore all further calls.
+   *
+   * @param options the options to be used for the end of the {@code Span}.
+   * @since 0.5
+   */
+  public abstract void end(EndSpanOptions options);
+
+  /**
+   * Marks the end of {@code Span} execution with the default options.
+   *
+   * <p>Only the timing of the first end call for a given {@code Span} will be recorded, and
+   * implementations are free to ignore all further calls.
+   *
+   * @since 0.5
+   */
+  public final void end() {
+    end(EndSpanOptions.DEFAULT);
+  }
+
+  /**
+   * Returns the {@code SpanContext} associated with this {@code Span}.
+   *
+   * @return the {@code SpanContext} associated with this {@code Span}.
+   * @since 0.5
+   */
+  public final SpanContext getContext() {
+    return context;
+  }
+
+  /**
+   * Returns the options associated with this {@code Span}.
+   *
+   * @return the options associated with this {@code Span}.
+   * @since 0.5
+   */
+  public final Set<Options> getOptions() {
+    return options;
+  }
+
+  /**
+   * Type of span. Can be used to specify additional relationships between spans in addition to a
+   * parent/child relationship.
+   *
+   * @since 0.14
+   */
+  public enum Kind {
+    /**
+     * Indicates that the span covers server-side handling of an RPC or other remote request.
+     *
+     * @since 0.14
+     */
+    SERVER,
+
+    /**
+     * Indicates that the span covers the client-side wrapper around an RPC or other remote request.
+     *
+     * @since 0.14
+     */
+    CLIENT
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/SpanBuilder.java b/api/src/main/java/io/opencensus/trace/SpanBuilder.java
new file mode 100644
index 0000000..f3a436a
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/SpanBuilder.java
@@ -0,0 +1,356 @@
+/*
+ * 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.trace;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import io.opencensus.common.Scope;
+import io.opencensus.internal.Utils;
+import java.util.List;
+import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
+
+/**
+ * {@link SpanBuilder} is used to construct {@link Span} instances which define arbitrary scopes of
+ * code that are sampled for distributed tracing as a single atomic unit.
+ *
+ * <p>This is a simple example where all the work is being done within a single scope and a single
+ * thread and the Context is automatically propagated:
+ *
+ * <pre>{@code
+ * class MyClass {
+ *   private static final Tracer tracer = Tracing.getTracer();
+ *   void doWork {
+ *     // Create a Span as a child of the current Span.
+ *     try (Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan()) {
+ *       tracer.getCurrentSpan().addAnnotation("my annotation");
+ *       doSomeWork();  // Here the new span is in the current Context, so it can be used
+ *                      // implicitly anywhere down the stack.
+ *     }
+ *   }
+ * }
+ * }</pre>
+ *
+ * <p>There might be cases where you do not perform all the work inside one static scope and the
+ * Context is automatically propagated:
+ *
+ * <pre>{@code
+ * class MyRpcServerInterceptorListener implements RpcServerInterceptor.Listener {
+ *   private static final Tracer tracer = Tracing.getTracer();
+ *   private Span mySpan;
+ *
+ *   public MyRpcInterceptor() {}
+ *
+ *   public void onRequest(String rpcName, Metadata metadata) {
+ *     // Create a Span as a child of the remote Span.
+ *     mySpan = tracer.spanBuilderWithRemoteParent(
+ *         getTraceContextFromMetadata(metadata), rpcName).startSpan();
+ *   }
+ *
+ *   public void onExecuteHandler(ServerCallHandler serverCallHandler) {
+ *     try (Scope ws = tracer.withSpan(mySpan)) {
+ *       tracer.getCurrentSpan().addAnnotation("Start rpc execution.");
+ *       serverCallHandler.run();  // Here the new span is in the current Context, so it can be
+ *                                 // used implicitly anywhere down the stack.
+ *     }
+ *   }
+ *
+ *   // Called when the RPC is canceled and guaranteed onComplete will not be called.
+ *   public void onCancel() {
+ *     // IMPORTANT: DO NOT forget to ended the Span here as the work is done.
+ *     mySpan.end(EndSpanOptions.builder().setStatus(Status.CANCELLED));
+ *   }
+ *
+ *   // Called when the RPC is done and guaranteed onCancel will not be called.
+ *   public void onComplete(RpcStatus rpcStatus) {
+ *     // IMPORTANT: DO NOT forget to ended the Span here as the work is done.
+ *     mySpan.end(EndSpanOptions.builder().setStatus(rpcStatusToCanonicalTraceStatus(status));
+ *   }
+ * }
+ * }</pre>
+ *
+ * <p>This is a simple example where all the work is being done within a single scope and the
+ * Context is manually propagated:
+ *
+ * <pre>{@code
+ * class MyClass {
+ *   private static final Tracer tracer = Tracing.getTracer();
+ *   void DoWork(Span parent) {
+ *     Span childSpan = tracer.spanBuilderWithExplicitParent("MyChildSpan", parent).startSpan();
+ *     childSpan.addAnnotation("my annotation");
+ *     try {
+ *       doSomeWork(childSpan); // Manually propagate the new span down the stack.
+ *     } finally {
+ *       // To make sure we end the span even in case of an exception.
+ *       childSpan.end();  // Manually end the span.
+ *     }
+ *   }
+ * }
+ * }</pre>
+ *
+ * <p>If your Java version is less than Java SE 7, see {@link SpanBuilder#startSpan} and {@link
+ * SpanBuilder#startScopedSpan} for usage examples.
+ *
+ * @since 0.5
+ */
+public abstract class SpanBuilder {
+
+  /**
+   * Sets the {@link Sampler} to use. If not set, the implementation will provide a default.
+   *
+   * @param sampler the {@code Sampler} to use when determining sampling for a {@code Span}.
+   * @return this.
+   * @since 0.5
+   */
+  public abstract SpanBuilder setSampler(Sampler sampler);
+
+  /**
+   * Sets the {@code List} of parent links. Links are used to link {@link Span}s in different
+   * traces. Used (for example) in batching operations, where a single batch handler processes
+   * multiple requests from different traces.
+   *
+   * @param parentLinks new links to be added.
+   * @return this.
+   * @throws NullPointerException if {@code parentLinks} is {@code null}.
+   * @since 0.5
+   */
+  public abstract SpanBuilder setParentLinks(List<Span> parentLinks);
+
+  /**
+   * Sets the option {@link Span.Options#RECORD_EVENTS} for the newly created {@code Span}. If not
+   * called, the implementation will provide a default.
+   *
+   * @param recordEvents new value determining if this {@code Span} should have events recorded.
+   * @return this.
+   * @since 0.5
+   */
+  public abstract SpanBuilder setRecordEvents(boolean recordEvents);
+
+  /**
+   * Sets the {@link Span.Kind} for the newly created {@code Span}. If not called, the
+   * implementation will provide a default.
+   *
+   * @param spanKind the kind of the newly created {@code Span}.
+   * @return this.
+   * @since 0.14
+   */
+  public SpanBuilder setSpanKind(@Nullable Span.Kind spanKind) {
+    return this;
+  }
+
+  /**
+   * Starts a new {@link Span}.
+   *
+   * <p>Users <b>must</b> manually call {@link Span#end()} or {@link Span#end(EndSpanOptions)} to
+   * end this {@code Span}.
+   *
+   * <p>Does not install the newly created {@code Span} to the current Context.
+   *
+   * <p>Example of usage:
+   *
+   * <pre>{@code
+   * class MyClass {
+   *   private static final Tracer tracer = Tracing.getTracer();
+   *   void DoWork(Span parent) {
+   *     Span childSpan = tracer.spanBuilderWithExplicitParent("MyChildSpan", parent).startSpan();
+   *     childSpan.addAnnotation("my annotation");
+   *     try {
+   *       doSomeWork(childSpan); // Manually propagate the new span down the stack.
+   *     } finally {
+   *       // To make sure we end the span even in case of an exception.
+   *       childSpan.end();  // Manually end the span.
+   *     }
+   *   }
+   * }
+   * }</pre>
+   *
+   * @return the newly created {@code Span}.
+   * @since 0.5
+   */
+  public abstract Span startSpan();
+
+  /**
+   * Starts a new span and sets it as the {@link Tracer#getCurrentSpan 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 Span#end}.
+   *
+   * <p>Supports try-with-resource idiom.
+   *
+   * <p>Example of usage:
+   *
+   * <pre>{@code
+   * class MyClass {
+   *   private static final Tracer tracer = Tracing.getTracer();
+   *   void doWork {
+   *     // Create a Span as a child of the current Span.
+   *     try (Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan()) {
+   *       tracer.getCurrentSpan().addAnnotation("my annotation");
+   *       doSomeWork();  // Here the new span is in the current Context, so it can be used
+   *                      // implicitly anywhere down the stack. Anytime in this closure the span
+   *                      // can be accessed via tracer.getCurrentSpan().
+   *     }
+   *   }
+   * }
+   * }</pre>
+   *
+   * <p>Prior to Java SE 7, you can use a finally block to ensure that a resource is closed (the
+   * {@code Span} is ended and removed from the Context) regardless of whether the try statement
+   * completes normally or abruptly.
+   *
+   * <p>Example of usage prior to Java SE7:
+   *
+   * <pre>{@code
+   * class MyClass {
+   *   private static Tracer tracer = Tracing.getTracer();
+   *   void doWork {
+   *     // Create a Span as a child of the current Span.
+   *     Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan();
+   *     try {
+   *       tracer.getCurrentSpan().addAnnotation("my annotation");
+   *       doSomeWork();  // Here the new span is in the current Context, so it can be used
+   *                      // implicitly anywhere down the stack. Anytime in this closure the span
+   *                      // can be accessed via tracer.getCurrentSpan().
+   *     } finally {
+   *       ss.close();
+   *     }
+   *   }
+   * }
+   * }</pre>
+   *
+   * <p>WARNING: The try-with-resources feature to auto-close spans as described above can sound
+   * very tempting due to its convenience, but it comes with an important and easy-to-miss
+   * trade-off: the span will be closed before any {@code catch} or {@code finally} blocks get a
+   * chance to execute. So if you need to catch any exceptions and log information about them (for
+   * example), then you do not want to use the try-with-resources shortcut because that logging will
+   * not be tagged with the span info of the span it logically falls under, and if you try to
+   * retrieve {@code Tracer.getCurrentSpan()} then you'll either get the parent span if one exists
+   * or {@code BlankSpan} if there was no parent span. This can be confusing and seem
+   * counter-intuitive, but it's the way try-with-resources works.
+   *
+   * @return an object that defines a scope where the newly created {@code Span} will be set to the
+   *     current Context.
+   * @since 0.5
+   */
+  @MustBeClosed
+  public final Scope startScopedSpan() {
+    return CurrentSpanUtils.withSpan(startSpan(), /* endSpan= */ true);
+  }
+
+  /**
+   * Starts a new span and runs the given {@code Runnable} with the newly created {@code Span} as
+   * the current {@code Span}, and ends the {@code Span} after the {@code Runnable} is run.
+   *
+   * <p>Any error will end up as a {@link Status#UNKNOWN}.
+   *
+   * <pre><code>
+   * tracer.spanBuilder("MyRunnableSpan").startSpanAndRun(myRunnable);
+   * </code></pre>
+   *
+   * <p>It is equivalent with the following code:
+   *
+   * <pre><code>
+   * Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+   * Runnable newRunnable = tracer.withSpan(span, myRunnable);
+   * try {
+   *   newRunnable.run();
+   * } finally {
+   *   span.end();
+   * }
+   * </code></pre>
+   *
+   * @param runnable the {@code Runnable} to run in the {@code Span}.
+   * @since 0.11.0
+   */
+  public final void startSpanAndRun(final Runnable runnable) {
+    final Span span = startSpan();
+    CurrentSpanUtils.withSpan(span, /* endSpan= */ true, runnable).run();
+  }
+
+  /**
+   * Starts a new span and calls the given {@code Callable} with the newly created {@code Span} as
+   * the current {@code Span}, and ends the {@code Span} after the {@code Callable} is called.
+   *
+   * <p>Any error will end up as a {@link Status#UNKNOWN}.
+   *
+   * <pre><code>
+   * MyResult myResult = tracer.spanBuilder("MyCallableSpan").startSpanAndCall(myCallable);
+   * </code></pre>
+   *
+   * <p>It is equivalent with the following code:
+   *
+   * <pre><code>
+   * Span span = tracer.spanBuilder("MyCallableSpan").startSpan();
+   * {@code Callable<MyResult>} newCallable = tracer.withSpan(span, myCallable);
+   * MyResult myResult = null;
+   * try {
+   *   myResult = newCallable.call();
+   * } finally {
+   *   span.end();
+   * }
+   * );
+   * </code></pre>
+   *
+   * @param callable the {@code Callable} to run in the {@code Span}.
+   * @since 0.11.0
+   */
+  public final <V> V startSpanAndCall(Callable<V> callable) throws Exception {
+    final Span span = startSpan();
+    return CurrentSpanUtils.withSpan(span, /* endSpan= */ true, callable).call();
+  }
+
+  static final class NoopSpanBuilder extends SpanBuilder {
+    static NoopSpanBuilder createWithParent(String spanName, @Nullable Span parent) {
+      return new NoopSpanBuilder(spanName);
+    }
+
+    static NoopSpanBuilder createWithRemoteParent(
+        String spanName, @Nullable SpanContext remoteParentSpanContext) {
+      return new NoopSpanBuilder(spanName);
+    }
+
+    @Override
+    public Span startSpan() {
+      return BlankSpan.INSTANCE;
+    }
+
+    @Override
+    public SpanBuilder setSampler(@Nullable Sampler sampler) {
+      return this;
+    }
+
+    @Override
+    public SpanBuilder setParentLinks(List<Span> parentLinks) {
+      return this;
+    }
+
+    @Override
+    public SpanBuilder setRecordEvents(boolean recordEvents) {
+      return this;
+    }
+
+    @Override
+    public SpanBuilder setSpanKind(@Nullable Span.Kind spanKind) {
+      return this;
+    }
+
+    private NoopSpanBuilder(String name) {
+      Utils.checkNotNull(name, "name");
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/SpanContext.java b/api/src/main/java/io/opencensus/trace/SpanContext.java
new file mode 100644
index 0000000..49ed751
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/SpanContext.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import java.util.Arrays;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a span context. A span context contains the state that must propagate to
+ * child {@link Span}s and across process boundaries. It contains the identifiers (a {@link TraceId
+ * trace_id} and {@link SpanId span_id}) associated with the {@link Span} and a set of {@link
+ * TraceOptions options}.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class SpanContext {
+  private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+  private final TraceId traceId;
+  private final SpanId spanId;
+  private final TraceOptions traceOptions;
+  private final Tracestate tracestate;
+
+  /**
+   * The invalid {@code SpanContext}.
+   *
+   * @since 0.5
+   */
+  public static final SpanContext INVALID =
+      new SpanContext(TraceId.INVALID, SpanId.INVALID, TraceOptions.DEFAULT, TRACESTATE_DEFAULT);
+
+  /**
+   * Creates a new {@code SpanContext} with the given identifiers and options.
+   *
+   * @param traceId the trace identifier of the span context.
+   * @param spanId the span identifier of the span context.
+   * @param traceOptions the trace options for the span context.
+   * @return a new {@code SpanContext} with the given identifiers and options.
+   * @deprecated use {@link #create(TraceId, SpanId, TraceOptions, Tracestate)}.
+   */
+  @Deprecated
+  public static SpanContext create(TraceId traceId, SpanId spanId, TraceOptions traceOptions) {
+    return create(traceId, spanId, traceOptions, TRACESTATE_DEFAULT);
+  }
+
+  /**
+   * Creates a new {@code SpanContext} with the given identifiers and options.
+   *
+   * @param traceId the trace identifier of the span context.
+   * @param spanId the span identifier of the span context.
+   * @param traceOptions the trace options for the span context.
+   * @param tracestate the trace state for the span context.
+   * @return a new {@code SpanContext} with the given identifiers and options.
+   * @since 0.16
+   */
+  public static SpanContext create(
+      TraceId traceId, SpanId spanId, TraceOptions traceOptions, Tracestate tracestate) {
+    return new SpanContext(traceId, spanId, traceOptions, tracestate);
+  }
+
+  /**
+   * Returns the trace identifier associated with this {@code SpanContext}.
+   *
+   * @return the trace identifier associated with this {@code SpanContext}.
+   * @since 0.5
+   */
+  public TraceId getTraceId() {
+    return traceId;
+  }
+
+  /**
+   * Returns the span identifier associated with this {@code SpanContext}.
+   *
+   * @return the span identifier associated with this {@code SpanContext}.
+   * @since 0.5
+   */
+  public SpanId getSpanId() {
+    return spanId;
+  }
+
+  /**
+   * Returns the {@code TraceOptions} associated with this {@code SpanContext}.
+   *
+   * @return the {@code TraceOptions} associated with this {@code SpanContext}.
+   * @since 0.5
+   */
+  public TraceOptions getTraceOptions() {
+    return traceOptions;
+  }
+
+  /**
+   * Returns the {@code Tracestate} associated with this {@code SpanContext}.
+   *
+   * @return the {@code Tracestate} associated with this {@code SpanContext}.
+   * @since 0.5
+   */
+  public Tracestate getTracestate() {
+    return tracestate;
+  }
+
+  /**
+   * Returns true if this {@code SpanContext} is valid.
+   *
+   * @return true if this {@code SpanContext} is valid.
+   * @since 0.5
+   */
+  public boolean isValid() {
+    return traceId.isValid() && spanId.isValid();
+  }
+
+  @Override
+  public boolean equals(@Nullable Object obj) {
+    if (obj == this) {
+      return true;
+    }
+
+    if (!(obj instanceof SpanContext)) {
+      return false;
+    }
+
+    SpanContext that = (SpanContext) obj;
+    return traceId.equals(that.traceId)
+        && spanId.equals(that.spanId)
+        && traceOptions.equals(that.traceOptions);
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(new Object[] {traceId, spanId, traceOptions});
+  }
+
+  @Override
+  public String toString() {
+    return "SpanContext{traceId="
+        + traceId
+        + ", spanId="
+        + spanId
+        + ", traceOptions="
+        + traceOptions
+        + "}";
+  }
+
+  private SpanContext(
+      TraceId traceId, SpanId spanId, TraceOptions traceOptions, Tracestate tracestate) {
+    this.traceId = traceId;
+    this.spanId = spanId;
+    this.traceOptions = traceOptions;
+    this.tracestate = tracestate;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/SpanId.java b/api/src/main/java/io/opencensus/trace/SpanId.java
new file mode 100644
index 0000000..c43fa6b
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/SpanId.java
@@ -0,0 +1,214 @@
+/*
+ * 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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.Arrays;
+import java.util.Random;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a span identifier. A valid span identifier is an 8-byte array with at
+ * least one non-zero byte.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class SpanId implements Comparable<SpanId> {
+  /**
+   * The size in bytes of the {@code SpanId}.
+   *
+   * @since 0.5
+   */
+  public static final int SIZE = 8;
+
+  private static final int HEX_SIZE = 2 * SIZE;
+
+  /**
+   * The invalid {@code SpanId}. All bytes are 0.
+   *
+   * @since 0.5
+   */
+  public static final SpanId INVALID = new SpanId(new byte[SIZE]);
+
+  // The internal representation of the SpanId.
+  private final byte[] bytes;
+
+  private SpanId(byte[] bytes) {
+    this.bytes = bytes;
+  }
+
+  /**
+   * Returns a {@code SpanId} built from a byte representation.
+   *
+   * <p>Equivalent with:
+   *
+   * <pre>{@code
+   * SpanId.fromBytes(buffer, 0);
+   * }</pre>
+   *
+   * @param buffer the representation of the {@code SpanId}.
+   * @return a {@code SpanId} whose representation is given by the {@code buffer} parameter.
+   * @throws NullPointerException if {@code buffer} is null.
+   * @throws IllegalArgumentException if {@code buffer.length} is not {@link SpanId#SIZE}.
+   * @since 0.5
+   */
+  public static SpanId fromBytes(byte[] buffer) {
+    Utils.checkNotNull(buffer, "buffer");
+    Utils.checkArgument(
+        buffer.length == SIZE, "Invalid size: expected %s, got %s", SIZE, buffer.length);
+    byte[] bytesCopied = Arrays.copyOf(buffer, SIZE);
+    return new SpanId(bytesCopied);
+  }
+
+  /**
+   * Returns a {@code SpanId} whose representation is copied from the {@code src} beginning at the
+   * {@code srcOffset} offset.
+   *
+   * @param src the buffer where the representation of the {@code SpanId} is copied.
+   * @param srcOffset the offset in the buffer where the representation of the {@code SpanId}
+   *     begins.
+   * @return a {@code SpanId} whose representation is copied from the buffer.
+   * @throws NullPointerException if {@code src} is null.
+   * @throws IndexOutOfBoundsException if {@code srcOffset+SpanId.SIZE} is greater than {@code
+   *     src.length}.
+   * @since 0.5
+   */
+  public static SpanId fromBytes(byte[] src, int srcOffset) {
+    byte[] bytes = new byte[SIZE];
+    System.arraycopy(src, srcOffset, bytes, 0, SIZE);
+    return new SpanId(bytes);
+  }
+
+  /**
+   * Returns a {@code SpanId} built from a lowercase base16 representation.
+   *
+   * @param src the lowercase base16 representation.
+   * @return a {@code SpanId} built from a lowercase base16 representation.
+   * @throws NullPointerException if {@code src} is null.
+   * @throws IllegalArgumentException if {@code src.length} is not {@code 2 * SpanId.SIZE} OR if the
+   *     {@code str} has invalid characters.
+   * @since 0.11
+   */
+  public static SpanId fromLowerBase16(CharSequence src) {
+    Utils.checkArgument(
+        src.length() == HEX_SIZE, "Invalid size: expected %s, got %s", HEX_SIZE, src.length());
+    return new SpanId(LowerCaseBase16Encoding.decodeToBytes(src));
+  }
+
+  /**
+   * Generates a new random {@code SpanId}.
+   *
+   * @param random The random number generator.
+   * @return a valid new {@code SpanId}.
+   * @since 0.5
+   */
+  public static SpanId generateRandomId(Random random) {
+    byte[] bytes = new byte[SIZE];
+    do {
+      random.nextBytes(bytes);
+    } while (Arrays.equals(bytes, INVALID.bytes));
+    return new SpanId(bytes);
+  }
+
+  /**
+   * Returns the byte representation of the {@code SpanId}.
+   *
+   * @return the byte representation of the {@code SpanId}.
+   * @since 0.5
+   */
+  public byte[] getBytes() {
+    return Arrays.copyOf(bytes, SIZE);
+  }
+
+  /**
+   * Copies the byte array representations of the {@code SpanId} into the {@code dest} beginning at
+   * the {@code destOffset} offset.
+   *
+   * <p>Equivalent with (but faster because it avoids any new allocations):
+   *
+   * <pre>{@code
+   * System.arraycopy(getBytes(), 0, dest, destOffset, SpanId.SIZE);
+   * }</pre>
+   *
+   * @param dest the destination buffer.
+   * @param destOffset the starting offset in the destination buffer.
+   * @throws NullPointerException if {@code dest} is null.
+   * @throws IndexOutOfBoundsException if {@code destOffset+SpanId.SIZE} is greater than {@code
+   *     dest.length}.
+   * @since 0.5
+   */
+  public void copyBytesTo(byte[] dest, int destOffset) {
+    System.arraycopy(bytes, 0, dest, destOffset, SIZE);
+  }
+
+  /**
+   * Returns whether the span identifier is valid. A valid span identifier is an 8-byte array with
+   * at least one non-zero byte.
+   *
+   * @return {@code true} if the span identifier is valid.
+   * @since 0.5
+   */
+  public boolean isValid() {
+    return !Arrays.equals(bytes, INVALID.bytes);
+  }
+
+  /**
+   * Returns the lowercase base16 encoding of this {@code SpanId}.
+   *
+   * @return the lowercase base16 encoding of this {@code SpanId}.
+   * @since 0.11
+   */
+  public String toLowerBase16() {
+    return LowerCaseBase16Encoding.encodeToString(bytes);
+  }
+
+  @Override
+  public boolean equals(@Nullable Object obj) {
+    if (obj == this) {
+      return true;
+    }
+
+    if (!(obj instanceof SpanId)) {
+      return false;
+    }
+
+    SpanId that = (SpanId) obj;
+    return Arrays.equals(bytes, that.bytes);
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(bytes);
+  }
+
+  @Override
+  public String toString() {
+    return "SpanId{spanId=" + toLowerBase16() + "}";
+  }
+
+  @Override
+  public int compareTo(SpanId that) {
+    for (int i = 0; i < SIZE; i++) {
+      if (bytes[i] != that.bytes[i]) {
+        return bytes[i] < that.bytes[i] ? -1 : 1;
+      }
+    }
+    return 0;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/Status.java b/api/src/main/java/io/opencensus/trace/Status.java
new file mode 100644
index 0000000..1fa8508
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Status.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.TreeMap;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/*>>>
+import org.checkerframework.dataflow.qual.Deterministic;
+*/
+
+/**
+ * Defines the status of a {@link Span} by providing a standard {@link CanonicalCode} in conjunction
+ * with an optional descriptive message. Instances of {@code Status} are created by starting with
+ * the template for the appropriate {@link Status.CanonicalCode} and supplementing it with
+ * additional information: {@code Status.NOT_FOUND.withDescription("Could not find
+ * 'important_file.txt'");}
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class Status {
+  /**
+   * The set of canonical status codes. If new codes are added over time they must choose a
+   * numerical value that does not collide with any previously used value.
+   *
+   * @since 0.5
+   */
+  public enum CanonicalCode {
+    /**
+     * The operation completed successfully.
+     *
+     * @since 0.5
+     */
+    OK(0),
+
+    /**
+     * The operation was cancelled (typically by the caller).
+     *
+     * @since 0.5
+     */
+    CANCELLED(1),
+
+    /**
+     * Unknown error. An example of where this error may be returned is if a Status value received
+     * from another address space belongs to an error-space that is not known in this address space.
+     * Also errors raised by APIs that do not return enough error information may be converted to
+     * this error.
+     *
+     * @since 0.5
+     */
+    UNKNOWN(2),
+
+    /**
+     * Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION.
+     * INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the
+     * system (e.g., a malformed file name).
+     *
+     * @since 0.5
+     */
+    INVALID_ARGUMENT(3),
+
+    /**
+     * Deadline expired before operation could complete. For operations that change the state of the
+     * system, this error may be returned even if the operation has completed successfully. For
+     * example, a successful response from a server could have been delayed long enough for the
+     * deadline to expire.
+     *
+     * @since 0.5
+     */
+    DEADLINE_EXCEEDED(4),
+
+    /**
+     * Some requested entity (e.g., file or directory) was not found.
+     *
+     * @since 0.5
+     */
+    NOT_FOUND(5),
+
+    /**
+     * Some entity that we attempted to create (e.g., file or directory) already exists.
+     *
+     * @since 0.5
+     */
+    ALREADY_EXISTS(6),
+
+    /**
+     * The caller does not have permission to execute the specified operation. PERMISSION_DENIED
+     * must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED
+     * instead for those errors). PERMISSION_DENIED must not be used if the caller cannot be
+     * identified (use UNAUTHENTICATED instead for those errors).
+     *
+     * @since 0.5
+     */
+    PERMISSION_DENIED(7),
+
+    /**
+     * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
+     * is out of space.
+     *
+     * @since 0.5
+     */
+    RESOURCE_EXHAUSTED(8),
+
+    /**
+     * Operation was rejected because the system is not in a state required for the operation's
+     * execution. For example, directory to be deleted may be non-empty, an rmdir operation is
+     * applied to a non-directory, etc.
+     *
+     * <p>A litmus test that may help a service implementor in deciding between FAILED_PRECONDITION,
+     * ABORTED, and UNAVAILABLE: (a) Use UNAVAILABLE if the client can retry just the failing call.
+     * (b) Use ABORTED if the client should retry at a higher-level (e.g., restarting a
+     * read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client should not retry until
+     * the system state has been explicitly fixed. E.g., if an "rmdir" fails because the directory
+     * is non-empty, FAILED_PRECONDITION should be returned since the client should not retry unless
+     * they have first fixed up the directory by deleting files from it.
+     *
+     * @since 0.5
+     */
+    FAILED_PRECONDITION(9),
+
+    /**
+     * The operation was aborted, typically due to a concurrency issue like sequencer check
+     * failures, transaction aborts, etc.
+     *
+     * <p>See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE.
+     *
+     * @since 0.5
+     */
+    ABORTED(10),
+
+    /**
+     * Operation was attempted past the valid range. E.g., seeking or reading past end of file.
+     *
+     * <p>Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed if the system
+     * state changes. For example, a 32-bit file system will generate INVALID_ARGUMENT if asked to
+     * read at an offset that is not in the range [0,2^32-1], but it will generate OUT_OF_RANGE if
+     * asked to read from an offset past the current file size.
+     *
+     * <p>There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. We recommend
+     * using OUT_OF_RANGE (the more specific error) when it applies so that callers who are
+     * iterating through a space can easily look for an OUT_OF_RANGE error to detect when they are
+     * done.
+     *
+     * @since 0.5
+     */
+    OUT_OF_RANGE(11),
+
+    /**
+     * Operation is not implemented or not supported/enabled in this service.
+     *
+     * @since 0.5
+     */
+    UNIMPLEMENTED(12),
+
+    /**
+     * Internal errors. Means some invariants expected by underlying system has been broken. If you
+     * see one of these errors, something is very broken.
+     *
+     * @since 0.5
+     */
+    INTERNAL(13),
+
+    /**
+     * The service is currently unavailable. This is a most likely a transient condition and may be
+     * corrected by retrying with a backoff.
+     *
+     * <p>See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE.
+     *
+     * @since 0.5
+     */
+    UNAVAILABLE(14),
+
+    /**
+     * Unrecoverable data loss or corruption.
+     *
+     * @since 0.5
+     */
+    DATA_LOSS(15),
+
+    /**
+     * The request does not have valid authentication credentials for the operation.
+     *
+     * @since 0.5
+     */
+    UNAUTHENTICATED(16);
+
+    private final int value;
+
+    private CanonicalCode(int value) {
+      this.value = value;
+    }
+
+    /**
+     * Returns the numerical value of the code.
+     *
+     * @return the numerical value of the code.
+     * @since 0.5
+     */
+    public int value() {
+      return value;
+    }
+
+    /**
+     * Returns the status that has the current {@code CanonicalCode}..
+     *
+     * @return the status that has the current {@code CanonicalCode}.
+     * @since 0.5
+     */
+    public Status toStatus() {
+      return STATUS_LIST.get(value);
+    }
+  }
+
+  // Create the canonical list of Status instances indexed by their code values.
+  private static final List<Status> STATUS_LIST = buildStatusList();
+
+  private static List<Status> buildStatusList() {
+    TreeMap<Integer, Status> canonicalizer = new TreeMap<Integer, Status>();
+    for (CanonicalCode code : CanonicalCode.values()) {
+      Status replaced = canonicalizer.put(code.value(), new Status(code, null));
+      if (replaced != null) {
+        throw new IllegalStateException(
+            "Code value duplication between "
+                + replaced.getCanonicalCode().name()
+                + " & "
+                + code.name());
+      }
+    }
+    return Collections.unmodifiableList(new ArrayList<Status>(canonicalizer.values()));
+  }
+
+  // A pseudo-enum of Status instances mapped 1:1 with values in CanonicalCode. This simplifies
+  // construction patterns for derived instances of Status.
+  /**
+   * The operation completed successfully.
+   *
+   * @since 0.5
+   */
+  public static final Status OK = CanonicalCode.OK.toStatus();
+
+  /**
+   * The operation was cancelled (typically by the caller).
+   *
+   * @since 0.5
+   */
+  public static final Status CANCELLED = CanonicalCode.CANCELLED.toStatus();
+
+  /**
+   * Unknown error. See {@link CanonicalCode#UNKNOWN}.
+   *
+   * @since 0.5
+   */
+  public static final Status UNKNOWN = CanonicalCode.UNKNOWN.toStatus();
+
+  /**
+   * Client specified an invalid argument. See {@link CanonicalCode#INVALID_ARGUMENT}.
+   *
+   * @since 0.5
+   */
+  public static final Status INVALID_ARGUMENT = CanonicalCode.INVALID_ARGUMENT.toStatus();
+
+  /**
+   * Deadline expired before operation could complete. See {@link CanonicalCode#DEADLINE_EXCEEDED}.
+   *
+   * @since 0.5
+   */
+  public static final Status DEADLINE_EXCEEDED = CanonicalCode.DEADLINE_EXCEEDED.toStatus();
+
+  /**
+   * Some requested entity (e.g., file or directory) was not found.
+   *
+   * @since 0.5
+   */
+  public static final Status NOT_FOUND = CanonicalCode.NOT_FOUND.toStatus();
+
+  /**
+   * Some entity that we attempted to create (e.g., file or directory) already exists.
+   *
+   * @since 0.5
+   */
+  public static final Status ALREADY_EXISTS = CanonicalCode.ALREADY_EXISTS.toStatus();
+
+  /**
+   * The caller does not have permission to execute the specified operation. See {@link
+   * CanonicalCode#PERMISSION_DENIED}.
+   *
+   * @since 0.5
+   */
+  public static final Status PERMISSION_DENIED = CanonicalCode.PERMISSION_DENIED.toStatus();
+
+  /**
+   * The request does not have valid authentication credentials for the operation.
+   *
+   * @since 0.5
+   */
+  public static final Status UNAUTHENTICATED = CanonicalCode.UNAUTHENTICATED.toStatus();
+
+  /**
+   * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
+   * is out of space.
+   *
+   * @since 0.5
+   */
+  public static final Status RESOURCE_EXHAUSTED = CanonicalCode.RESOURCE_EXHAUSTED.toStatus();
+
+  /**
+   * Operation was rejected because the system is not in a state required for the operation's
+   * execution. See {@link CanonicalCode#FAILED_PRECONDITION}.
+   *
+   * @since 0.5
+   */
+  public static final Status FAILED_PRECONDITION = CanonicalCode.FAILED_PRECONDITION.toStatus();
+
+  /**
+   * The operation was aborted, typically due to a concurrency issue like sequencer check failures,
+   * transaction aborts, etc. See {@link CanonicalCode#ABORTED}.
+   *
+   * @since 0.5
+   */
+  public static final Status ABORTED = CanonicalCode.ABORTED.toStatus();
+
+  /**
+   * Operation was attempted past the valid range. See {@link CanonicalCode#OUT_OF_RANGE}.
+   *
+   * @since 0.5
+   */
+  public static final Status OUT_OF_RANGE = CanonicalCode.OUT_OF_RANGE.toStatus();
+
+  /**
+   * Operation is not implemented or not supported/enabled in this service.
+   *
+   * @since 0.5
+   */
+  public static final Status UNIMPLEMENTED = CanonicalCode.UNIMPLEMENTED.toStatus();
+
+  /**
+   * Internal errors. See {@link CanonicalCode#INTERNAL}.
+   *
+   * @since 0.5
+   */
+  public static final Status INTERNAL = CanonicalCode.INTERNAL.toStatus();
+
+  /**
+   * The service is currently unavailable. See {@link CanonicalCode#UNAVAILABLE}.
+   *
+   * @since 0.5
+   */
+  public static final Status UNAVAILABLE = CanonicalCode.UNAVAILABLE.toStatus();
+
+  /**
+   * Unrecoverable data loss or corruption.
+   *
+   * @since 0.5
+   */
+  public static final Status DATA_LOSS = CanonicalCode.DATA_LOSS.toStatus();
+
+  // The canonical code of this message.
+  private final CanonicalCode canonicalCode;
+
+  // An additional error message.
+  @Nullable private final String description;
+
+  private Status(CanonicalCode canonicalCode, @Nullable String description) {
+    this.canonicalCode = Utils.checkNotNull(canonicalCode, "canonicalCode");
+    this.description = description;
+  }
+
+  /**
+   * Creates a derived instance of {@code Status} with the given description.
+   *
+   * @param description the new description of the {@code Status}.
+   * @return The newly created {@code Status} with the given description.
+   * @since 0.5
+   */
+  public Status withDescription(String description) {
+    if (Utils.equalsObjects(this.description, description)) {
+      return this;
+    }
+    return new Status(this.canonicalCode, description);
+  }
+
+  /**
+   * Returns the canonical status code.
+   *
+   * @return the canonical status code.
+   * @since 0.5
+   */
+  public CanonicalCode getCanonicalCode() {
+    return canonicalCode;
+  }
+
+  /**
+   * Returns the description of this {@code Status} for human consumption.
+   *
+   * @return the description of this {@code Status}.
+   * @since 0.5
+   */
+  @Nullable
+  /*@Deterministic*/
+  public String getDescription() {
+    return description;
+  }
+
+  /**
+   * Returns {@code true} if this {@code Status} is OK, i.e., not an error.
+   *
+   * @return {@code true} if this {@code Status} is OK.
+   * @since 0.5
+   */
+  public boolean isOk() {
+    return CanonicalCode.OK == canonicalCode;
+  }
+
+  /**
+   * Equality on Statuses is not well defined. Instead, do comparison based on their CanonicalCode
+   * with {@link #getCanonicalCode}. The description of the Status is unlikely to be stable, and
+   * additional fields may be added to Status in the future.
+   */
+  @Override
+  public boolean equals(@Nullable Object obj) {
+    if (obj == this) {
+      return true;
+    }
+
+    if (!(obj instanceof Status)) {
+      return false;
+    }
+
+    Status that = (Status) obj;
+    return canonicalCode == that.canonicalCode
+        && Utils.equalsObjects(description, that.description);
+  }
+
+  /**
+   * Hash codes on Statuses are not well defined.
+   *
+   * @see #equals
+   */
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(new Object[] {canonicalCode, description});
+  }
+
+  @Override
+  public String toString() {
+    return "Status{canonicalCode=" + canonicalCode + ", description=" + description + "}";
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/TraceComponent.java b/api/src/main/java/io/opencensus/trace/TraceComponent.java
new file mode 100644
index 0000000..d98d0f9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/TraceComponent.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.internal.ZeroTimeClock;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/**
+ * Class that holds the implementation instances for {@link Tracer}, {@link PropagationComponent},
+ * {@link Clock}, {@link ExportComponent} and {@link TraceConfig}.
+ *
+ * <p>Unless otherwise noted all methods (on component) results are cacheable.
+ *
+ * @since 0.5
+ */
+public abstract class TraceComponent {
+
+  /**
+   * Returns the {@link Tracer} with the provided implementations. If no implementation is provided
+   * then no-op implementations will be used.
+   *
+   * @return the {@code Tracer} implementation.
+   * @since 0.5
+   */
+  public abstract Tracer getTracer();
+
+  /**
+   * Returns the {@link PropagationComponent} with the provided implementation. If no implementation
+   * is provided then no-op implementation will be used.
+   *
+   * @return the {@code PropagationComponent} implementation.
+   * @since 0.5
+   */
+  public abstract PropagationComponent getPropagationComponent();
+
+  /**
+   * Returns the {@link Clock} with the provided implementation.
+   *
+   * @return the {@code Clock} implementation.
+   * @since 0.5
+   */
+  public abstract Clock getClock();
+
+  /**
+   * Returns the {@link ExportComponent} with the provided implementation. If no implementation is
+   * provided then no-op implementations will be used.
+   *
+   * @return the {@link ExportComponent} implementation.
+   * @since 0.5
+   */
+  public abstract ExportComponent getExportComponent();
+
+  /**
+   * Returns the {@link TraceConfig} with the provided implementation. If no implementation is
+   * provided then no-op implementations will be used.
+   *
+   * @return the {@link TraceConfig} implementation.
+   * @since 0.5
+   */
+  public abstract TraceConfig getTraceConfig();
+
+  /**
+   * Returns an instance that contains no-op implementations for all the instances.
+   *
+   * @return an instance that contains no-op implementations for all the instances.
+   */
+  static TraceComponent newNoopTraceComponent() {
+    return new NoopTraceComponent();
+  }
+
+  private static final class NoopTraceComponent extends TraceComponent {
+    private final ExportComponent noopExportComponent = ExportComponent.newNoopExportComponent();
+
+    @Override
+    public Tracer getTracer() {
+      return Tracer.getNoopTracer();
+    }
+
+    @Override
+    public PropagationComponent getPropagationComponent() {
+      return PropagationComponent.getNoopPropagationComponent();
+    }
+
+    @Override
+    public Clock getClock() {
+      return ZeroTimeClock.getInstance();
+    }
+
+    @Override
+    public ExportComponent getExportComponent() {
+      return noopExportComponent;
+    }
+
+    @Override
+    public TraceConfig getTraceConfig() {
+      return TraceConfig.getNoopTraceConfig();
+    }
+
+    private NoopTraceComponent() {}
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/TraceId.java b/api/src/main/java/io/opencensus/trace/TraceId.java
new file mode 100644
index 0000000..465e4d4
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/TraceId.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import io.opencensus.common.Internal;
+import io.opencensus.internal.Utils;
+import java.util.Arrays;
+import java.util.Random;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a trace identifier. A valid trace identifier is a 16-byte array with at
+ * least one non-zero byte.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class TraceId implements Comparable<TraceId> {
+  /**
+   * The size in bytes of the {@code TraceId}.
+   *
+   * @since 0.5
+   */
+  public static final int SIZE = 16;
+
+  private static final int HEX_SIZE = 32;
+
+  /**
+   * The invalid {@code TraceId}. All bytes are '\0'.
+   *
+   * @since 0.5
+   */
+  public static final TraceId INVALID = new TraceId(new byte[SIZE]);
+
+  // The internal representation of the TraceId.
+  private final byte[] bytes;
+
+  private TraceId(byte[] bytes) {
+    this.bytes = bytes;
+  }
+
+  /**
+   * Returns a {@code TraceId} built from a byte representation.
+   *
+   * <p>Equivalent with:
+   *
+   * <pre>{@code
+   * TraceId.fromBytes(buffer, 0);
+   * }</pre>
+   *
+   * @param buffer the representation of the {@code TraceId}.
+   * @return a {@code TraceId} whose representation is given by the {@code buffer} parameter.
+   * @throws NullPointerException if {@code buffer} is null.
+   * @throws IllegalArgumentException if {@code buffer.length} is not {@link TraceId#SIZE}.
+   * @since 0.5
+   */
+  public static TraceId fromBytes(byte[] buffer) {
+    Utils.checkNotNull(buffer, "buffer");
+    Utils.checkArgument(
+        buffer.length == SIZE, "Invalid size: expected %s, got %s", SIZE, buffer.length);
+    byte[] bytesCopied = Arrays.copyOf(buffer, SIZE);
+    return new TraceId(bytesCopied);
+  }
+
+  /**
+   * Returns a {@code TraceId} whose representation is copied from the {@code src} beginning at the
+   * {@code srcOffset} offset.
+   *
+   * @param src the buffer where the representation of the {@code TraceId} is copied.
+   * @param srcOffset the offset in the buffer where the representation of the {@code TraceId}
+   *     begins.
+   * @return a {@code TraceId} whose representation is copied from the buffer.
+   * @throws NullPointerException if {@code src} is null.
+   * @throws IndexOutOfBoundsException if {@code srcOffset+TraceId.SIZE} is greater than {@code
+   *     src.length}.
+   * @since 0.5
+   */
+  public static TraceId fromBytes(byte[] src, int srcOffset) {
+    byte[] bytes = new byte[SIZE];
+    System.arraycopy(src, srcOffset, bytes, 0, SIZE);
+    return new TraceId(bytes);
+  }
+
+  /**
+   * Returns a {@code TraceId} built from a lowercase base16 representation.
+   *
+   * @param src the lowercase base16 representation.
+   * @return a {@code TraceId} built from a lowercase base16 representation.
+   * @throws NullPointerException if {@code src} is null.
+   * @throws IllegalArgumentException if {@code src.length} is not {@code 2 * TraceId.SIZE} OR if
+   *     the {@code str} has invalid characters.
+   * @since 0.11
+   */
+  public static TraceId fromLowerBase16(CharSequence src) {
+    Utils.checkArgument(
+        src.length() == HEX_SIZE, "Invalid size: expected %s, got %s", HEX_SIZE, src.length());
+    return new TraceId(LowerCaseBase16Encoding.decodeToBytes(src));
+  }
+
+  /**
+   * Generates a new random {@code TraceId}.
+   *
+   * @param random the random number generator.
+   * @return a new valid {@code TraceId}.
+   * @since 0.5
+   */
+  public static TraceId generateRandomId(Random random) {
+    byte[] bytes = new byte[SIZE];
+    do {
+      random.nextBytes(bytes);
+    } while (Arrays.equals(bytes, INVALID.bytes));
+    return new TraceId(bytes);
+  }
+
+  /**
+   * Returns the 16-bytes array representation of the {@code TraceId}.
+   *
+   * @return the 16-bytes array representation of the {@code TraceId}.
+   * @since 0.5
+   */
+  public byte[] getBytes() {
+    return Arrays.copyOf(bytes, SIZE);
+  }
+
+  /**
+   * Copies the byte array representations of the {@code TraceId} into the {@code dest} beginning at
+   * the {@code destOffset} offset.
+   *
+   * <p>Equivalent with (but faster because it avoids any new allocations):
+   *
+   * <pre>{@code
+   * System.arraycopy(getBytes(), 0, dest, destOffset, TraceId.SIZE);
+   * }</pre>
+   *
+   * @param dest the destination buffer.
+   * @param destOffset the starting offset in the destination buffer.
+   * @throws NullPointerException if {@code dest} is null.
+   * @throws IndexOutOfBoundsException if {@code destOffset+TraceId.SIZE} is greater than {@code
+   *     dest.length}.
+   * @since 0.5
+   */
+  public void copyBytesTo(byte[] dest, int destOffset) {
+    System.arraycopy(bytes, 0, dest, destOffset, SIZE);
+  }
+
+  /**
+   * Returns whether the {@code TraceId} is valid. A valid trace identifier is a 16-byte array with
+   * at least one non-zero byte.
+   *
+   * @return {@code true} if the {@code TraceId} is valid.
+   * @since 0.5
+   */
+  public boolean isValid() {
+    return !Arrays.equals(bytes, INVALID.bytes);
+  }
+
+  /**
+   * Returns the lowercase base16 encoding of this {@code TraceId}.
+   *
+   * @return the lowercase base16 encoding of this {@code TraceId}.
+   * @since 0.11
+   */
+  public String toLowerBase16() {
+    return LowerCaseBase16Encoding.encodeToString(bytes);
+  }
+
+  /**
+   * Returns the lower 8 bytes of the trace-id as a long value, assuming little-endian order. This
+   * is used in ProbabilitySampler.
+   *
+   * <p>This method is marked as internal and subject to change.
+   *
+   * @return the lower 8 bytes of the trace-id as a long value, assuming little-endian order.
+   */
+  @Internal
+  public long getLowerLong() {
+    long result = 0;
+    for (int i = 0; i < Long.SIZE / Byte.SIZE; i++) {
+      result <<= Byte.SIZE;
+      result |= (bytes[i] & 0xff);
+    }
+    if (result < 0) {
+      return -result;
+    }
+    return result;
+  }
+
+  @Override
+  public boolean equals(@Nullable Object obj) {
+    if (obj == this) {
+      return true;
+    }
+
+    if (!(obj instanceof TraceId)) {
+      return false;
+    }
+
+    TraceId that = (TraceId) obj;
+    return Arrays.equals(bytes, that.bytes);
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(bytes);
+  }
+
+  @Override
+  public String toString() {
+    return "TraceId{traceId=" + toLowerBase16() + "}";
+  }
+
+  @Override
+  public int compareTo(TraceId that) {
+    for (int i = 0; i < SIZE; i++) {
+      if (bytes[i] != that.bytes[i]) {
+        return bytes[i] < that.bytes[i] ? -1 : 1;
+      }
+    }
+    return 0;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/TraceOptions.java b/api/src/main/java/io/opencensus/trace/TraceOptions.java
new file mode 100644
index 0000000..218f4da
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/TraceOptions.java
@@ -0,0 +1,280 @@
+/*
+ * 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.trace;
+
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Utils;
+import java.util.Arrays;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents global trace options. These options are propagated to all child {@link
+ * io.opencensus.trace.Span spans}. These determine features such as whether a {@code Span} should
+ * be traced. It is implemented as a bitmask.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class TraceOptions {
+  // Default options. Nothing set.
+  private static final byte DEFAULT_OPTIONS = 0;
+  // Bit to represent whether trace is sampled or not.
+  private static final byte IS_SAMPLED = 0x1;
+
+  /**
+   * The size in bytes of the {@code TraceOptions}.
+   *
+   * @since 0.5
+   */
+  public static final int SIZE = 1;
+
+  /**
+   * The default {@code TraceOptions}.
+   *
+   * @since 0.5
+   */
+  public static final TraceOptions DEFAULT = fromByte(DEFAULT_OPTIONS);
+
+  // The set of enabled features is determined by all the enabled bits.
+  private final byte options;
+
+  // Creates a new {@code TraceOptions} with the given options.
+  private TraceOptions(byte options) {
+    this.options = options;
+  }
+
+  /**
+   * Returns a {@code TraceOptions} built from a byte representation.
+   *
+   * <p>Equivalent with:
+   *
+   * <pre>{@code
+   * TraceOptions.fromBytes(buffer, 0);
+   * }</pre>
+   *
+   * @param buffer the representation of the {@code TraceOptions}.
+   * @return a {@code TraceOptions} whose representation is given by the {@code buffer} parameter.
+   * @throws NullPointerException if {@code buffer} is null.
+   * @throws IllegalArgumentException if {@code buffer.length} is not {@link TraceOptions#SIZE}.
+   * @since 0.5
+   * @deprecated use {@link #fromByte(byte)}.
+   */
+  @Deprecated
+  public static TraceOptions fromBytes(byte[] buffer) {
+    Utils.checkNotNull(buffer, "buffer");
+    Utils.checkArgument(
+        buffer.length == SIZE, "Invalid size: expected %s, got %s", SIZE, buffer.length);
+    return fromByte(buffer[0]);
+  }
+
+  /**
+   * Returns a {@code TraceOptions} whose representation is copied from the {@code src} beginning at
+   * the {@code srcOffset} offset.
+   *
+   * @param src the buffer where the representation of the {@code TraceOptions} is copied.
+   * @param srcOffset the offset in the buffer where the representation of the {@code TraceOptions}
+   *     begins.
+   * @return a {@code TraceOptions} whose representation is copied from the buffer.
+   * @throws NullPointerException if {@code src} is null.
+   * @throws IndexOutOfBoundsException if {@code srcOffset+TraceOptions.SIZE} is greater than {@code
+   *     src.length}.
+   * @since 0.5
+   * @deprecated use {@link #fromByte(byte)}.
+   */
+  @Deprecated
+  public static TraceOptions fromBytes(byte[] src, int srcOffset) {
+    Utils.checkIndex(srcOffset, src.length);
+    return fromByte(src[srcOffset]);
+  }
+
+  /**
+   * Returns a {@code TraceOptions} whose representation is {@code src}.
+   *
+   * @param src the byte representation of the {@code TraceOptions}.
+   * @return a {@code TraceOptions} whose representation is {@code src}.
+   * @since 0.16
+   */
+  public static TraceOptions fromByte(byte src) {
+    // TODO(bdrutu): OPTIMIZATION: Cache all the 256 possible objects and return from the cache.
+    return new TraceOptions(src);
+  }
+
+  /**
+   * Returns the one byte representation of the {@code TraceOptions}.
+   *
+   * @return the one byte representation of the {@code TraceOptions}.
+   * @since 0.16
+   */
+  public byte getByte() {
+    return options;
+  }
+
+  /**
+   * Returns the 1-byte array representation of the {@code TraceOptions}.
+   *
+   * @return the 1-byte array representation of the {@code TraceOptions}.
+   * @since 0.5
+   * @deprecated use {@link #getByte()}.
+   */
+  @Deprecated
+  public byte[] getBytes() {
+    byte[] bytes = new byte[SIZE];
+    bytes[0] = options;
+    return bytes;
+  }
+
+  /**
+   * Copies the byte representations of the {@code TraceOptions} into the {@code dest} beginning at
+   * the {@code destOffset} offset.
+   *
+   * <p>Equivalent with (but faster because it avoids any new allocations):
+   *
+   * <pre>{@code
+   * System.arraycopy(getBytes(), 0, dest, destOffset, TraceOptions.SIZE);
+   * }</pre>
+   *
+   * @param dest the destination buffer.
+   * @param destOffset the starting offset in the destination buffer.
+   * @throws NullPointerException if {@code dest} is null.
+   * @throws IndexOutOfBoundsException if {@code destOffset+TraceOptions.SIZE} is greater than
+   *     {@code dest.length}.
+   * @since 0.5
+   */
+  public void copyBytesTo(byte[] dest, int destOffset) {
+    Utils.checkIndex(destOffset, dest.length);
+    dest[destOffset] = options;
+  }
+
+  /**
+   * Returns a new {@link Builder} with default options.
+   *
+   * @return a new {@code Builder} with default options.
+   * @since 0.5
+   */
+  public static Builder builder() {
+    return new Builder(DEFAULT_OPTIONS);
+  }
+
+  /**
+   * Returns a new {@link Builder} with all given options set.
+   *
+   * @param traceOptions the given options set.
+   * @return a new {@code Builder} with all given options set.
+   * @since 0.5
+   */
+  public static Builder builder(TraceOptions traceOptions) {
+    return new Builder(traceOptions.options);
+  }
+
+  /**
+   * Returns a boolean indicating whether this {@code Span} is part of a sampled trace and data
+   * should be exported to a persistent store.
+   *
+   * @return a boolean indicating whether the trace is sampled.
+   * @since 0.5
+   */
+  public boolean isSampled() {
+    return hasOption(IS_SAMPLED);
+  }
+
+  @Override
+  public boolean equals(@Nullable Object obj) {
+    if (obj == this) {
+      return true;
+    }
+
+    if (!(obj instanceof TraceOptions)) {
+      return false;
+    }
+
+    TraceOptions that = (TraceOptions) obj;
+    return options == that.options;
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(new byte[] {options});
+  }
+
+  @Override
+  public String toString() {
+    return "TraceOptions{sampled=" + isSampled() + "}";
+  }
+
+  /**
+   * Builder class for {@link TraceOptions}.
+   *
+   * @since 0.5
+   */
+  public static final class Builder {
+    private byte options;
+
+    private Builder(byte options) {
+      this.options = options;
+    }
+
+    /**
+     * Sets the sampling bit in the options to true.
+     *
+     * @deprecated Use {@code Builder.setIsSampled(true)}.
+     * @return this.
+     * @since 0.5
+     */
+    @Deprecated
+    public Builder setIsSampled() {
+      return setIsSampled(true);
+    }
+
+    /**
+     * Sets the sampling bit in the options.
+     *
+     * @param isSampled the sampling bit.
+     * @return this.
+     * @since 0.7
+     */
+    public Builder setIsSampled(boolean isSampled) {
+      if (isSampled) {
+        options = (byte) (options | IS_SAMPLED);
+      } else {
+        options = (byte) (options & ~IS_SAMPLED);
+        ;
+      }
+      return this;
+    }
+
+    /**
+     * Builds and returns a {@code TraceOptions} with the desired options.
+     *
+     * @return a {@code TraceOptions} with the desired options.
+     * @since 0.5
+     */
+    public TraceOptions build() {
+      return fromByte(options);
+    }
+  }
+
+  // Returns the current set of options bitmask.
+  @DefaultVisibilityForTesting
+  byte getOptions() {
+    return options;
+  }
+
+  private boolean hasOption(int mask) {
+    return (this.options & mask) != 0;
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/Tracer.java b/api/src/main/java/io/opencensus/trace/Tracer.java
new file mode 100644
index 0000000..a2c0a23
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Tracer.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import io.opencensus.common.Scope;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.SpanBuilder.NoopSpanBuilder;
+import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
+
+/**
+ * Tracer is a simple, thin class for {@link Span} creation and in-process context interaction.
+ *
+ * <p>Users may choose to use manual or automatic Context propagation. Because of that this class
+ * offers APIs to facilitate both usages.
+ *
+ * <p>The automatic context propagation is done using {@link io.grpc.Context} which is a gRPC
+ * independent implementation for in-process Context propagation mechanism which can carry
+ * scoped-values across API boundaries and between threads. Users of the library must propagate the
+ * {@link io.grpc.Context} between different threads.
+ *
+ * <p>Example usage with automatic context propagation:
+ *
+ * <pre>{@code
+ * class MyClass {
+ *   private static final Tracer tracer = Tracing.getTracer();
+ *   void doWork() {
+ *     try(Scope ss = tracer.spanBuilder("MyClass.DoWork").startScopedSpan()) {
+ *       tracer.getCurrentSpan().addAnnotation("Starting the work.");
+ *       doWorkInternal();
+ *       tracer.getCurrentSpan().addAnnotation("Finished working.");
+ *     }
+ *   }
+ * }
+ * }</pre>
+ *
+ * <p>Example usage with manual context propagation:
+ *
+ * <pre>{@code
+ * class MyClass {
+ *   private static final Tracer tracer = Tracing.getTracer();
+ *   void doWork(Span parent) {
+ *     Span childSpan = tracer.spanBuilderWithExplicitParent("MyChildSpan", parent).startSpan();
+ *     childSpan.addAnnotation("Starting the work.");
+ *     try {
+ *       doSomeWork(childSpan); // Manually propagate the new span down the stack.
+ *     } finally {
+ *       // To make sure we end the span even in case of an exception.
+ *       childSpan.end();  // Manually end the span.
+ *     }
+ *   }
+ * }
+ * }</pre>
+ *
+ * @since 0.5
+ */
+public abstract class Tracer {
+  private static final NoopTracer noopTracer = new NoopTracer();
+
+  /**
+   * Returns the no-op implementation of the {@code Tracer}.
+   *
+   * @return the no-op implementation of the {@code Tracer}.
+   */
+  static Tracer getNoopTracer() {
+    return noopTracer;
+  }
+
+  /**
+   * Gets the current Span from the current Context.
+   *
+   * <p>To install a {@link Span} to the current Context use {@link #withSpan(Span)} OR use {@link
+   * SpanBuilder#startScopedSpan} methods to start a new {@code Span}.
+   *
+   * <p>startSpan methods do NOT modify the current Context {@code Span}.
+   *
+   * @return a default {@code Span} that does nothing and has an invalid {@link SpanContext} if no
+   *     {@code Span} is associated with the current Context, otherwise the current {@code Span}
+   *     from the Context.
+   * @since 0.5
+   */
+  public final Span getCurrentSpan() {
+    Span currentSpan = CurrentSpanUtils.getCurrentSpan();
+    return currentSpan != null ? currentSpan : BlankSpan.INSTANCE;
+  }
+
+  /**
+   * Enters the scope of code where the given {@link Span} is in the current Context, and returns an
+   * object that represents that scope. The scope is exited when the returned object is closed.
+   *
+   * <p>Supports try-with-resource idiom.
+   *
+   * <p>Can be called with {@link BlankSpan} to enter a scope of code where tracing is stopped.
+   *
+   * <p>Example of usage:
+   *
+   * <pre>{@code
+   * private static Tracer tracer = Tracing.getTracer();
+   * void doWork() {
+   *   // Create a Span as a child of the current Span.
+   *   Span span = tracer.spanBuilder("my span").startSpan();
+   *   try (Scope ws = tracer.withSpan(span)) {
+   *     tracer.getCurrentSpan().addAnnotation("my annotation");
+   *     doSomeOtherWork();  // Here "span" is the current Span.
+   *   }
+   *   span.end();
+   * }
+   * }</pre>
+   *
+   * <p>Prior to Java SE 7, you can use a finally block to ensure that a resource is closed
+   * regardless of whether the try statement completes normally or abruptly.
+   *
+   * <p>Example of usage prior to Java SE7:
+   *
+   * <pre>{@code
+   * private static Tracer tracer = Tracing.getTracer();
+   * void doWork() {
+   *   // Create a Span as a child of the current Span.
+   *   Span span = tracer.spanBuilder("my span").startSpan();
+   *   Scope ws = tracer.withSpan(span);
+   *   try {
+   *     tracer.getCurrentSpan().addAnnotation("my annotation");
+   *     doSomeOtherWork();  // Here "span" is the current Span.
+   *   } finally {
+   *     ws.close();
+   *   }
+   *   span.end();
+   * }
+   * }</pre>
+   *
+   * @param span The {@link Span} to be set to the current Context.
+   * @return an object that defines a scope where the given {@link Span} will be set to the current
+   *     Context.
+   * @throws NullPointerException if {@code span} is {@code null}.
+   * @since 0.5
+   */
+  @MustBeClosed
+  public final Scope withSpan(Span span) {
+    return CurrentSpanUtils.withSpan(Utils.checkNotNull(span, "span"), /* endSpan= */ false);
+  }
+
+  /**
+   * Returns a {@link Runnable} that runs the given task with the given {@code Span} in the current
+   * context.
+   *
+   * <p>Users may consider to use {@link SpanBuilder#startSpanAndRun(Runnable)}.
+   *
+   * <p>Any error will end up as a {@link Status#UNKNOWN}.
+   *
+   * <p>IMPORTANT: Caller must manually propagate the entire {@code io.grpc.Context} when wraps a
+   * {@code Runnable}, see the examples.
+   *
+   * <p>IMPORTANT: Caller must manually end the {@code Span} within the {@code Runnable}, or after
+   * the {@code Runnable} is executed.
+   *
+   * <p>Example with Executor wrapped with {@link io.grpc.Context#currentContextExecutor}:
+   *
+   * <pre><code>
+   * class MyClass {
+   *   private static Tracer tracer = Tracing.getTracer();
+   *   void handleRequest(Executor executor) {
+   *     Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+   *     executor.execute(tracer.withSpan(span, new Runnable() {
+   *      {@literal @}Override
+   *       public void run() {
+   *         try {
+   *           sendResult();
+   *         } finally {
+   *           span.end();
+   *         }
+   *       }
+   *     }));
+   *   }
+   * }
+   * </code></pre>
+   *
+   * <p>Example without Executor wrapped with {@link io.grpc.Context#currentContextExecutor}:
+   *
+   * <pre><code>
+   * class MyClass {
+   *   private static Tracer tracer = Tracing.getTracer();
+   *   void handleRequest(Executor executor) {
+   *     Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+   *     executor.execute(Context.wrap(tracer.withSpan(span, new Runnable() {
+   *      {@literal @}Override
+   *       public void run() {
+   *         try {
+   *           sendResult();
+   *         } finally {
+   *           span.end();
+   *         }
+   *       }
+   *     })));
+   *   }
+   * }
+   * </code></pre>
+   *
+   * @param span the {@code Span} to be set as current.
+   * @param runnable the {@code Runnable} to withSpan in the {@code Span}.
+   * @return the {@code Runnable}.
+   * @since 0.11.0
+   */
+  public final Runnable withSpan(Span span, Runnable runnable) {
+    return CurrentSpanUtils.withSpan(span, /* endSpan= */ false, runnable);
+  }
+
+  /**
+   * Returns a {@link Callable} that runs the given task with the given {@code Span} in the current
+   * context.
+   *
+   * <p>Users may consider to use {@link SpanBuilder#startSpanAndCall(Callable)}.
+   *
+   * <p>Any error will end up as a {@link Status#UNKNOWN}.
+   *
+   * <p>IMPORTANT: Caller must manually propagate the entire {@code io.grpc.Context} when wraps a
+   * {@code Callable}, see the examples.
+   *
+   * <p>IMPORTANT: Caller must manually end the {@code Span} within the {@code Callable}, or after
+   * the {@code Callable} is executed.
+   *
+   * <p>Example with Executor wrapped with {@link io.grpc.Context#currentContextExecutor}:
+   *
+   * <pre><code>
+   * class MyClass {
+   *   private static Tracer tracer = Tracing.getTracer();
+   *   void handleRequest(Executor executor) {
+   *     Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+   *     executor.execute(tracer.withSpan(span, {@code new Callable<MyResult>()} {
+   *      {@literal @}Override
+   *       public MyResult call() throws Exception {
+   *         try {
+   *           return sendResult();
+   *         } finally {
+   *           span.end();
+   *         }
+   *       }
+   *     }));
+   *   }
+   * }
+   * </code></pre>
+   *
+   * <p>Example without Executor wrapped with {@link io.grpc.Context#currentContextExecutor}:
+   *
+   * <pre><code>
+   * class MyClass {
+   *   private static Tracer tracer = Tracing.getTracer();
+   *   void handleRequest(Executor executor) {
+   *     Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+   *     executor.execute(Context.wrap(tracer.withSpan(span, {@code new Callable<MyResult>()} {
+   *      {@literal @}Override
+   *       public MyResult call() throws Exception {
+   *         try {
+   *           return sendResult();
+   *         } finally {
+   *           span.end();
+   *         }
+   *       }
+   *     })));
+   *   }
+   * }
+   * </code></pre>
+   *
+   * @param span the {@code Span} to be set as current.
+   * @param callable the {@code Callable} to run in the {@code Span}.
+   * @return the {@code Callable}.
+   * @since 0.11.0
+   */
+  public final <C> Callable<C> withSpan(Span span, final Callable<C> callable) {
+    return CurrentSpanUtils.withSpan(span, /* endSpan= */ false, callable);
+  }
+
+  /**
+   * Returns a {@link SpanBuilder} to create and start a new child {@link Span} as a child of to the
+   * current {@code Span} if any, otherwise creates a root {@code Span}.
+   *
+   * <p>See {@link SpanBuilder} for usage examples.
+   *
+   * <p>This <b>must</b> be used to create a {@code Span} when automatic Context propagation is
+   * used.
+   *
+   * <p>This is equivalent with:
+   *
+   * <pre>{@code
+   * tracer.spanBuilderWithExplicitParent("MySpanName",tracer.getCurrentSpan());
+   * }</pre>
+   *
+   * @param spanName The name of the returned Span.
+   * @return a {@code SpanBuilder} to create and start a new {@code Span}.
+   * @throws NullPointerException if {@code spanName} is {@code null}.
+   * @since 0.5
+   */
+  public final SpanBuilder spanBuilder(String spanName) {
+    return spanBuilderWithExplicitParent(spanName, CurrentSpanUtils.getCurrentSpan());
+  }
+
+  /**
+   * Returns a {@link SpanBuilder} to create and start a new child {@link Span} (or root if parent
+   * is {@code null} or has an invalid {@link SpanContext}), with parent being the designated {@code
+   * Span}.
+   *
+   * <p>See {@link SpanBuilder} for usage examples.
+   *
+   * <p>This <b>must</b> be used to create a {@code Span} when manual Context propagation is used OR
+   * when creating a root {@code Span} with a {@code null} parent.
+   *
+   * @param spanName The name of the returned Span.
+   * @param parent The parent of the returned Span. If {@code null} the {@code SpanBuilder} will
+   *     build a root {@code Span}.
+   * @return a {@code SpanBuilder} to create and start a new {@code Span}.
+   * @throws NullPointerException if {@code spanName} is {@code null}.
+   * @since 0.5
+   */
+  public abstract SpanBuilder spanBuilderWithExplicitParent(String spanName, @Nullable Span parent);
+
+  /**
+   * Returns a {@link SpanBuilder} to create and start a new child {@link Span} (or root if parent
+   * is {@link SpanContext#INVALID} or {@code null}), with parent being the remote {@link Span}
+   * designated by the {@link SpanContext}.
+   *
+   * <p>See {@link SpanBuilder} for usage examples.
+   *
+   * <p>This <b>must</b> be used to create a {@code Span} when the parent is in a different process.
+   * This is only intended for use by RPC systems or similar.
+   *
+   * <p>If no {@link SpanContext} OR fail to parse the {@link SpanContext} on the server side, users
+   * must call this method with a {@code null} remote parent {@code SpanContext}.
+   *
+   * @param spanName The name of the returned Span.
+   * @param remoteParentSpanContext The remote parent of the returned Span.
+   * @return a {@code SpanBuilder} to create and start a new {@code Span}.
+   * @throws NullPointerException if {@code spanName} is {@code null}.
+   * @since 0.5
+   */
+  public abstract SpanBuilder spanBuilderWithRemoteParent(
+      String spanName, @Nullable SpanContext remoteParentSpanContext);
+
+  // No-Op implementation of the Tracer.
+  private static final class NoopTracer extends Tracer {
+
+    @Override
+    public SpanBuilder spanBuilderWithExplicitParent(String spanName, @Nullable Span parent) {
+      return NoopSpanBuilder.createWithParent(spanName, parent);
+    }
+
+    @Override
+    public SpanBuilder spanBuilderWithRemoteParent(
+        String spanName, @Nullable SpanContext remoteParentSpanContext) {
+      return NoopSpanBuilder.createWithRemoteParent(spanName, remoteParentSpanContext);
+    }
+
+    private NoopTracer() {}
+  }
+
+  protected Tracer() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/Tracestate.java b/api/src/main/java/io/opencensus/trace/Tracestate.java
new file mode 100644
index 0000000..dae587c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Tracestate.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Carries tracing-system specific context in a list of key-value pairs. TraceState allows different
+ * vendors propagate additional information and inter-operate with their legacy Id formats.
+ *
+ * <p>Implementation is optimized for a small list of key-value pairs.
+ *
+ * <p>Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter,
+ * and can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and
+ * forward slashes /.
+ *
+ * <p>Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the
+ * range 0x20 to 0x7E) except comma , and =.
+ *
+ * @since 0.16
+ */
+@Immutable
+@AutoValue
+@ExperimentalApi
+public abstract class Tracestate {
+  private static final int KEY_MAX_SIZE = 256;
+  private static final int VALUE_MAX_SIZE = 256;
+  private static final int MAX_KEY_VALUE_PAIRS = 32;
+
+  /**
+   * Returns the value to which the specified key is mapped, or null if this map contains no mapping
+   * for the key.
+   *
+   * @param key with which the specified value is to be associated
+   * @return the value to which the specified key is mapped, or null if this map contains no mapping
+   *     for the key.
+   * @since 0.16
+   */
+  @javax.annotation.Nullable
+  public String get(String key) {
+    for (Entry entry : getEntries()) {
+      if (entry.getKey().equals(key)) {
+        return entry.getValue();
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns a {@link List} view of the mappings contained in this {@code TraceState}.
+   *
+   * @return a {@link List} view of the mappings contained in this {@code TraceState}.
+   * @since 0.16
+   */
+  public abstract List<Entry> getEntries();
+
+  /**
+   * Returns a {@code Builder} based on an empty {@code Tracestate}.
+   *
+   * @return a {@code Builder} based on an empty {@code Tracestate}.
+   * @since 0.16
+   */
+  public static Builder builder() {
+    return new Builder(Builder.EMPTY);
+  }
+
+  /**
+   * Returns a {@code Builder} based on this {@code Tracestate}.
+   *
+   * @return a {@code Builder} based on this {@code Tracestate}.
+   * @since 0.16
+   */
+  public Builder toBuilder() {
+    return new Builder(this);
+  }
+
+  /**
+   * Builder class for {@link MessageEvent}.
+   *
+   * @since 0.16
+   */
+  @ExperimentalApi
+  public static final class Builder {
+    private final Tracestate parent;
+    @javax.annotation.Nullable private ArrayList<Entry> entries;
+
+    // Needs to be in this class to avoid initialization deadlock because super class depends on
+    // subclass (the auto-value generate class).
+    private static final Tracestate EMPTY = create(Collections.<Entry>emptyList());
+
+    private Builder(Tracestate parent) {
+      Utils.checkNotNull(parent, "parent");
+      this.parent = parent;
+      this.entries = null;
+    }
+
+    /**
+     * Adds or updates the {@code Entry} that has the given {@code key} if it is present. The new
+     * {@code Entry} will always be added in the front of the list of entries.
+     *
+     * @param key the key for the {@code Entry} to be added.
+     * @param value the value for the {@code Entry} to be added.
+     * @return this.
+     * @since 0.16
+     */
+    @SuppressWarnings("nullness")
+    public Builder set(String key, String value) {
+      // Initially create the Entry to validate input.
+      Entry entry = Entry.create(key, value);
+      if (entries == null) {
+        // Copy entries from the parent.
+        entries = new ArrayList<Entry>(parent.getEntries());
+      }
+      for (int i = 0; i < entries.size(); i++) {
+        if (entries.get(i).getKey().equals(entry.getKey())) {
+          entries.remove(i);
+          // Exit now because the entries list cannot contain duplicates.
+          break;
+        }
+      }
+      // Inserts the element at the front of this list.
+      entries.add(0, entry);
+      return this;
+    }
+
+    /**
+     * Removes the {@code Entry} that has the given {@code key} if it is present.
+     *
+     * @param key the key for the {@code Entry} to be removed.
+     * @return this.
+     * @since 0.16
+     */
+    @SuppressWarnings("nullness")
+    public Builder remove(String key) {
+      Utils.checkNotNull(key, "key");
+      if (entries == null) {
+        // Copy entries from the parent.
+        entries = new ArrayList<Entry>(parent.getEntries());
+      }
+      for (int i = 0; i < entries.size(); i++) {
+        if (entries.get(i).getKey().equals(key)) {
+          entries.remove(i);
+          // Exit now because the entries list cannot contain duplicates.
+          break;
+        }
+      }
+      return this;
+    }
+
+    /**
+     * Builds a TraceState by adding the entries to the parent in front of the key-value pairs list
+     * and removing duplicate entries.
+     *
+     * @return a TraceState with the new entries.
+     * @since 0.16
+     */
+    public Tracestate build() {
+      if (entries == null) {
+        return parent;
+      }
+      return Tracestate.create(entries);
+    }
+  }
+
+  /**
+   * Immutable key-value pair for {@code Tracestate}.
+   *
+   * @since 0.16
+   */
+  @Immutable
+  @AutoValue
+  @ExperimentalApi
+  public abstract static class Entry {
+    /**
+     * Creates a new {@code Entry} for the {@code Tracestate}.
+     *
+     * @param key the Entry's key.
+     * @param value the Entry's value.
+     * @since 0.16
+     */
+    public static Entry create(String key, String value) {
+      Utils.checkNotNull(key, "key");
+      Utils.checkNotNull(value, "value");
+      Utils.checkArgument(validateKey(key), "Invalid key %s", key);
+      Utils.checkArgument(validateValue(value), "Invalid value %s", value);
+      return new AutoValue_Tracestate_Entry(key, value);
+    }
+
+    /**
+     * Returns the key {@code String}.
+     *
+     * @return the key {@code String}.
+     * @since 0.16
+     */
+    public abstract String getKey();
+
+    /**
+     * Returns the value {@code String}.
+     *
+     * @return the value {@code String}.
+     * @since 0.16
+     */
+    public abstract String getValue();
+
+    Entry() {}
+  }
+
+  // Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter, and
+  // can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and
+  // forward slashes /.
+  private static boolean validateKey(String key) {
+    if (key.length() > KEY_MAX_SIZE
+        || key.isEmpty()
+        || key.charAt(0) < 'a'
+        || key.charAt(0) > 'z') {
+      return false;
+    }
+    for (int i = 1; i < key.length(); i++) {
+      char c = key.charAt(i);
+      if (!(c >= 'a' && c <= 'z')
+          && !(c >= '0' && c <= '9')
+          && c != '_'
+          && c != '-'
+          && c != '*'
+          && c != '/') {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the range
+  // 0x20 to 0x7E) except comma , and =.
+  private static boolean validateValue(String value) {
+    if (value.length() > VALUE_MAX_SIZE || value.charAt(value.length() - 1) == ' ' /* '\u0020' */) {
+      return false;
+    }
+    for (int i = 0; i < value.length(); i++) {
+      char c = value.charAt(i);
+      if (c == ',' || c == '=' || c < ' ' /* '\u0020' */ || c > '~' /* '\u007E' */) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static Tracestate create(List<Entry> entries) {
+    Utils.checkState(entries.size() <= MAX_KEY_VALUE_PAIRS, "Invalid size");
+    return new AutoValue_Tracestate(Collections.unmodifiableList(entries));
+  }
+
+  Tracestate() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/Tracing.java b/api/src/main/java/io/opencensus/trace/Tracing.java
new file mode 100644
index 0000000..f55cd77
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Tracing.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Provider;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * Class that manages a global instance of the {@link TraceComponent}.
+ *
+ * @since 0.5
+ */
+public final class Tracing {
+  private static final Logger logger = Logger.getLogger(Tracing.class.getName());
+  private static final TraceComponent traceComponent =
+      loadTraceComponent(TraceComponent.class.getClassLoader());
+
+  /**
+   * Returns the global {@link Tracer}.
+   *
+   * @return the global {@code Tracer}.
+   * @since 0.5
+   */
+  public static Tracer getTracer() {
+    return traceComponent.getTracer();
+  }
+
+  /**
+   * Returns the global {@link PropagationComponent}.
+   *
+   * @return the global {@code PropagationComponent}.
+   * @since 0.5
+   */
+  public static PropagationComponent getPropagationComponent() {
+    return traceComponent.getPropagationComponent();
+  }
+
+  /**
+   * Returns the global {@link Clock}.
+   *
+   * @return the global {@code Clock}.
+   * @since 0.5
+   */
+  public static Clock getClock() {
+    return traceComponent.getClock();
+  }
+
+  /**
+   * Returns the global {@link ExportComponent}.
+   *
+   * @return the global {@code ExportComponent}.
+   * @since 0.5
+   */
+  public static ExportComponent getExportComponent() {
+    return traceComponent.getExportComponent();
+  }
+
+  /**
+   * Returns the global {@link TraceConfig}.
+   *
+   * @return the global {@code TraceConfig}.
+   * @since 0.5
+   */
+  public static TraceConfig getTraceConfig() {
+    return traceComponent.getTraceConfig();
+  }
+
+  // Any provider that may be used for TraceComponent can be added here.
+  @DefaultVisibilityForTesting
+  static TraceComponent loadTraceComponent(@Nullable ClassLoader classLoader) {
+    try {
+      // Call Class.forName with literal string name of the class to help shading tools.
+      return Provider.createInstance(
+          Class.forName(
+              "io.opencensus.impl.trace.TraceComponentImpl", /*initialize=*/ true, classLoader),
+          TraceComponent.class);
+    } catch (ClassNotFoundException e) {
+      logger.log(
+          Level.FINE,
+          "Couldn't load full implementation for TraceComponent, now trying to load lite "
+              + "implementation.",
+          e);
+    }
+    try {
+      // Call Class.forName with literal string name of the class to help shading tools.
+      return Provider.createInstance(
+          Class.forName(
+              "io.opencensus.impllite.trace.TraceComponentImplLite",
+              /*initialize=*/ true,
+              classLoader),
+          TraceComponent.class);
+    } catch (ClassNotFoundException e) {
+      logger.log(
+          Level.FINE,
+          "Couldn't load lite implementation for TraceComponent, now using "
+              + "default implementation for TraceComponent.",
+          e);
+    }
+    return TraceComponent.newNoopTraceComponent();
+  }
+
+  // No instance of this class.
+  private Tracing() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/config/TraceConfig.java b/api/src/main/java/io/opencensus/trace/config/TraceConfig.java
new file mode 100644
index 0000000..ff701e2
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/config/TraceConfig.java
@@ -0,0 +1,64 @@
+/*
+ * 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.trace.config;
+
+/**
+ * Global configuration of the trace service. This allows users to change configs for the default
+ * sampler, maximum events to be kept, etc. (see {@link TraceParams} for details).
+ *
+ * @since 0.5
+ */
+public abstract class TraceConfig {
+  private static final NoopTraceConfig NOOP_TRACE_CONFIG = new NoopTraceConfig();
+
+  /**
+   * Returns the active {@code TraceParams}.
+   *
+   * @return the active {@code TraceParams}.
+   * @since 0.5
+   */
+  public abstract TraceParams getActiveTraceParams();
+
+  /**
+   * Updates the active {@link TraceParams}.
+   *
+   * @param traceParams the new active {@code TraceParams}.
+   * @since 0.5
+   */
+  public abstract void updateActiveTraceParams(TraceParams traceParams);
+
+  /**
+   * Returns the no-op implementation of the {@code TraceConfig}.
+   *
+   * @return the no-op implementation of the {@code TraceConfig}.
+   * @since 0.5
+   */
+  public static TraceConfig getNoopTraceConfig() {
+    return NOOP_TRACE_CONFIG;
+  }
+
+  private static final class NoopTraceConfig extends TraceConfig {
+
+    @Override
+    public TraceParams getActiveTraceParams() {
+      return TraceParams.DEFAULT;
+    }
+
+    @Override
+    public void updateActiveTraceParams(TraceParams traceParams) {}
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/config/TraceParams.java b/api/src/main/java/io/opencensus/trace/config/TraceParams.java
new file mode 100644
index 0000000..ff70f52
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/config/TraceParams.java
@@ -0,0 +1,219 @@
+/*
+ * 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.trace.config;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.samplers.Samplers;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Class that holds global trace parameters.
+ *
+ * @since 0.5
+ */
+@AutoValue
+@Immutable
+public abstract class TraceParams {
+  // These values are the default values for all the global parameters.
+  private static final double DEFAULT_PROBABILITY = 1e-4;
+  private static final Sampler DEFAULT_SAMPLER = Samplers.probabilitySampler(DEFAULT_PROBABILITY);
+  private static final int DEFAULT_SPAN_MAX_NUM_ATTRIBUTES = 32;
+  private static final int DEFAULT_SPAN_MAX_NUM_ANNOTATIONS = 32;
+  private static final int DEFAULT_SPAN_MAX_NUM_MESSAGE_EVENTS = 128;
+  private static final int DEFAULT_SPAN_MAX_NUM_LINKS = 32;
+
+  /**
+   * Default {@code TraceParams}.
+   *
+   * @since 0.5
+   */
+  public static final TraceParams DEFAULT =
+      TraceParams.builder()
+          .setSampler(DEFAULT_SAMPLER)
+          .setMaxNumberOfAttributes(DEFAULT_SPAN_MAX_NUM_ATTRIBUTES)
+          .setMaxNumberOfAnnotations(DEFAULT_SPAN_MAX_NUM_ANNOTATIONS)
+          .setMaxNumberOfMessageEvents(DEFAULT_SPAN_MAX_NUM_MESSAGE_EVENTS)
+          .setMaxNumberOfLinks(DEFAULT_SPAN_MAX_NUM_LINKS)
+          .build();
+
+  /**
+   * Returns the global default {@code Sampler}. Used if no {@code Sampler} is provided in {@link
+   * io.opencensus.trace.SpanBuilder#setSampler(Sampler)}.
+   *
+   * @return the global default {@code Sampler}.
+   * @since 0.5
+   */
+  public abstract Sampler getSampler();
+
+  /**
+   * Returns the global default max number of attributes per {@link Span}.
+   *
+   * @return the global default max number of attributes per {@link Span}.
+   * @since 0.5
+   */
+  public abstract int getMaxNumberOfAttributes();
+
+  /**
+   * Returns the global default max number of {@link Annotation} events per {@link Span}.
+   *
+   * @return the global default max number of {@code Annotation} events per {@code Span}.
+   * @since 0.5
+   */
+  public abstract int getMaxNumberOfAnnotations();
+
+  /**
+   * Returns the global default max number of {@link MessageEvent} events per {@link Span}.
+   *
+   * @return the global default max number of {@code MessageEvent} events per {@code Span}.
+   * @since 0.12
+   */
+  public abstract int getMaxNumberOfMessageEvents();
+
+  /**
+   * Returns the global default max number of {@link io.opencensus.trace.NetworkEvent} events per
+   * {@link Span}.
+   *
+   * @return the global default max number of {@code NetworkEvent} events per {@code Span}.
+   * @deprecated Use {@link getMaxNumberOfMessageEvents}.
+   * @since 0.5
+   */
+  @Deprecated
+  public int getMaxNumberOfNetworkEvents() {
+    return getMaxNumberOfMessageEvents();
+  }
+
+  /**
+   * Returns the global default max number of {@link Link} entries per {@link Span}.
+   *
+   * @return the global default max number of {@code Link} entries per {@code Span}.
+   * @since 0.5
+   */
+  public abstract int getMaxNumberOfLinks();
+
+  private static Builder builder() {
+    return new AutoValue_TraceParams.Builder();
+  }
+
+  /**
+   * Returns a {@link Builder} initialized to the same property values as the current instance.
+   *
+   * @return a {@link Builder} initialized to the same property values as the current instance.
+   * @since 0.5
+   */
+  public abstract Builder toBuilder();
+
+  /**
+   * A {@code Builder} class for {@link TraceParams}.
+   *
+   * @since 0.5
+   */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    /**
+     * Sets the global default {@code Sampler}. It must be not {@code null} otherwise {@link
+     * #build()} will throw an exception.
+     *
+     * @param sampler the global default {@code Sampler}.
+     * @return this.
+     * @since 0.5
+     */
+    public abstract Builder setSampler(Sampler sampler);
+
+    /**
+     * Sets the global default max number of attributes per {@link Span}.
+     *
+     * @param maxNumberOfAttributes the global default max number of attributes per {@link Span}. It
+     *     must be positive otherwise {@link #build()} will throw an exception.
+     * @return this.
+     * @since 0.5
+     */
+    public abstract Builder setMaxNumberOfAttributes(int maxNumberOfAttributes);
+
+    /**
+     * Sets the global default max number of {@link Annotation} events per {@link Span}.
+     *
+     * @param maxNumberOfAnnotations the global default max number of {@link Annotation} events per
+     *     {@link Span}. It must be positive otherwise {@link #build()} will throw an exception.
+     * @return this.
+     * @since 0.5
+     */
+    public abstract Builder setMaxNumberOfAnnotations(int maxNumberOfAnnotations);
+
+    /**
+     * Sets the global default max number of {@link MessageEvent} events per {@link Span}.
+     *
+     * @param maxNumberOfMessageEvents the global default max number of {@link MessageEvent} events
+     *     per {@link Span}. It must be positive otherwise {@link #build()} will throw an exception.
+     * @since 0.12
+     * @return this.
+     */
+    public abstract Builder setMaxNumberOfMessageEvents(int maxNumberOfMessageEvents);
+
+    /**
+     * Sets the global default max number of {@link io.opencensus.trace.NetworkEvent} events per
+     * {@link Span}.
+     *
+     * @param maxNumberOfNetworkEvents the global default max number of {@link
+     *     io.opencensus.trace.NetworkEvent} events per {@link Span}. It must be positive otherwise
+     *     {@link #build()} will throw an exception.
+     * @return this.
+     * @deprecated Use {@link setMaxNumberOfMessageEvents}.
+     * @since 0.5
+     */
+    @Deprecated
+    public Builder setMaxNumberOfNetworkEvents(int maxNumberOfNetworkEvents) {
+      return setMaxNumberOfMessageEvents(maxNumberOfNetworkEvents);
+    }
+
+    /**
+     * Sets the global default max number of {@link Link} entries per {@link Span}.
+     *
+     * @param maxNumberOfLinks the global default max number of {@link Link} entries per {@link
+     *     Span}. It must be positive otherwise {@link #build()} will throw an exception.
+     * @return this.
+     * @since 0.5
+     */
+    public abstract Builder setMaxNumberOfLinks(int maxNumberOfLinks);
+
+    abstract TraceParams autoBuild();
+
+    /**
+     * Builds and returns a {@code TraceParams} with the desired values.
+     *
+     * @return a {@code TraceParams} with the desired values.
+     * @throws NullPointerException if the sampler is {@code null}.
+     * @throws IllegalArgumentException if any of the max numbers are not positive.
+     * @since 0.5
+     */
+    public TraceParams build() {
+      TraceParams traceParams = autoBuild();
+      Utils.checkArgument(traceParams.getMaxNumberOfAttributes() > 0, "maxNumberOfAttributes");
+      Utils.checkArgument(traceParams.getMaxNumberOfAnnotations() > 0, "maxNumberOfAnnotations");
+      Utils.checkArgument(
+          traceParams.getMaxNumberOfMessageEvents() > 0, "maxNumberOfMessageEvents");
+      Utils.checkArgument(traceParams.getMaxNumberOfLinks() > 0, "maxNumberOfLinks");
+      return traceParams;
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/ExportComponent.java b/api/src/main/java/io/opencensus/trace/export/ExportComponent.java
new file mode 100644
index 0000000..c334c5a
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/ExportComponent.java
@@ -0,0 +1,95 @@
+/*
+ * 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.trace.export;
+
+import io.opencensus.trace.TraceOptions;
+
+/**
+ * Class that holds the implementation instances for {@link SpanExporter}, {@link RunningSpanStore}
+ * and {@link SampledSpanStore}.
+ *
+ * <p>Unless otherwise noted all methods (on component) results are cacheable.
+ *
+ * @since 0.5
+ */
+public abstract class ExportComponent {
+
+  /**
+   * Returns the no-op implementation of the {@code ExportComponent}.
+   *
+   * @return the no-op implementation of the {@code ExportComponent}.
+   * @since 0.5
+   */
+  public static ExportComponent newNoopExportComponent() {
+    return new NoopExportComponent();
+  }
+
+  /**
+   * Returns the {@link SpanExporter} which can be used to register handlers to export all the spans
+   * that are part of a distributed sampled trace (see {@link TraceOptions#isSampled()}).
+   *
+   * @return the implementation of the {@code SpanExporter} or no-op if no implementation linked in
+   *     the binary.
+   * @since 0.5
+   */
+  public abstract SpanExporter getSpanExporter();
+
+  /**
+   * Returns the {@link RunningSpanStore} that can be used to get useful debugging information about
+   * all the current active spans.
+   *
+   * @return the {@code RunningSpanStore}.
+   * @since 0.5
+   */
+  public abstract RunningSpanStore getRunningSpanStore();
+
+  /**
+   * Returns the {@link SampledSpanStore} that can be used to get useful debugging information, such
+   * as latency based sampled spans, error based sampled spans.
+   *
+   * @return the {@code SampledSpanStore}.
+   * @since 0.5
+   */
+  public abstract SampledSpanStore getSampledSpanStore();
+
+  /**
+   * Will shutdown this ExportComponent after flushing any pending spans.
+   *
+   * @since 0.14
+   */
+  public void shutdown() {}
+
+  private static final class NoopExportComponent extends ExportComponent {
+    private final SampledSpanStore noopSampledSpanStore =
+        SampledSpanStore.newNoopSampledSpanStore();
+
+    @Override
+    public SpanExporter getSpanExporter() {
+      return SpanExporter.getNoopSpanExporter();
+    }
+
+    @Override
+    public RunningSpanStore getRunningSpanStore() {
+      return RunningSpanStore.getNoopRunningSpanStore();
+    }
+
+    @Override
+    public SampledSpanStore getSampledSpanStore() {
+      return noopSampledSpanStore;
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/RunningSpanStore.java b/api/src/main/java/io/opencensus/trace/export/RunningSpanStore.java
new file mode 100644
index 0000000..fac3c85
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/RunningSpanStore.java
@@ -0,0 +1,201 @@
+/*
+ * 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.trace.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * This class allows users to access in-process information about all running spans.
+ *
+ * <p>The running spans tracking is available for all the spans with the option {@link
+ * io.opencensus.trace.Span.Options#RECORD_EVENTS}. This functionality allows users to debug stuck
+ * operations or long living operations.
+ *
+ * @since 0.5
+ */
+@ThreadSafe
+public abstract class RunningSpanStore {
+
+  private static final RunningSpanStore NOOP_RUNNING_SPAN_STORE = new NoopRunningSpanStore();
+
+  protected RunningSpanStore() {}
+
+  /**
+   * Returns the no-op implementation of the {@code RunningSpanStore}.
+   *
+   * @return the no-op implementation of the {@code RunningSpanStore}.
+   */
+  static RunningSpanStore getNoopRunningSpanStore() {
+    return NOOP_RUNNING_SPAN_STORE;
+  }
+
+  /**
+   * Returns the summary of all available data such, as number of running spans.
+   *
+   * @return the summary of all available data.
+   * @since 0.5
+   */
+  public abstract Summary getSummary();
+
+  /**
+   * Returns a list of running spans that match the {@code Filter}.
+   *
+   * @param filter used to filter the returned spans.
+   * @return a list of running spans that match the {@code Filter}.
+   * @since 0.5
+   */
+  public abstract Collection<SpanData> getRunningSpans(Filter filter);
+
+  /**
+   * The summary of all available data.
+   *
+   * @since 0.5
+   */
+  @AutoValue
+  @Immutable
+  public abstract static class Summary {
+
+    Summary() {}
+
+    /**
+     * Returns a new instance of {@code Summary}.
+     *
+     * @param perSpanNameSummary a map with summary for each span name.
+     * @return a new instance of {@code Summary}.
+     * @throws NullPointerException if {@code perSpanNameSummary} is {@code null}.
+     * @since 0.5
+     */
+    public static Summary create(Map<String, PerSpanNameSummary> perSpanNameSummary) {
+      return new AutoValue_RunningSpanStore_Summary(
+          Collections.unmodifiableMap(
+              new HashMap<String, PerSpanNameSummary>(
+                  Utils.checkNotNull(perSpanNameSummary, "perSpanNameSummary"))));
+    }
+
+    /**
+     * Returns a map with summary of available data for each span name.
+     *
+     * @return a map with all the span names and the summary.
+     * @since 0.5
+     */
+    public abstract Map<String, PerSpanNameSummary> getPerSpanNameSummary();
+  }
+
+  /**
+   * Summary of all available data for a span name.
+   *
+   * @since 0.5
+   */
+  @AutoValue
+  @Immutable
+  public abstract static class PerSpanNameSummary {
+
+    PerSpanNameSummary() {}
+
+    /**
+     * Returns a new instance of {@code PerSpanNameSummary}.
+     *
+     * @param numRunningSpans the number of running spans.
+     * @return a new instance of {@code PerSpanNameSummary}.
+     * @throws IllegalArgumentException if {@code numRunningSpans} is negative.
+     * @since 0.5
+     */
+    public static PerSpanNameSummary create(int numRunningSpans) {
+      Utils.checkArgument(numRunningSpans >= 0, "Negative numRunningSpans.");
+      return new AutoValue_RunningSpanStore_PerSpanNameSummary(numRunningSpans);
+    }
+
+    /**
+     * Returns the number of running spans.
+     *
+     * @return the number of running spans.
+     * @since 0.5
+     */
+    public abstract int getNumRunningSpans();
+  }
+
+  /**
+   * Filter for running spans. Used to filter results returned by the {@link
+   * #getRunningSpans(Filter)} request.
+   *
+   * @since 0.5
+   */
+  @AutoValue
+  @Immutable
+  public abstract static class Filter {
+
+    Filter() {}
+
+    /**
+     * Returns a new instance of {@code Filter}.
+     *
+     * <p>Filters all the spans based on {@code spanName} and returns a maximum of {@code
+     * maxSpansToReturn}.
+     *
+     * @param spanName the name of the span.
+     * @param maxSpansToReturn the maximum number of results to be returned. {@code 0} means all.
+     * @return a new instance of {@code Filter}.
+     * @throws NullPointerException if {@code spanName} is {@code null}.
+     * @throws IllegalArgumentException if {@code maxSpansToReturn} is negative.
+     * @since 0.5
+     */
+    public static Filter create(String spanName, int maxSpansToReturn) {
+      Utils.checkArgument(maxSpansToReturn >= 0, "Negative maxSpansToReturn.");
+      return new AutoValue_RunningSpanStore_Filter(spanName, maxSpansToReturn);
+    }
+
+    /**
+     * Returns the span name.
+     *
+     * @return the span name.
+     * @since 0.5
+     */
+    public abstract String getSpanName();
+
+    /**
+     * Returns the maximum number of spans to be returned. {@code 0} means all.
+     *
+     * @return the maximum number of spans to be returned.
+     * @since 0.5
+     */
+    public abstract int getMaxSpansToReturn();
+  }
+
+  private static final class NoopRunningSpanStore extends RunningSpanStore {
+
+    private static final Summary EMPTY_SUMMARY =
+        Summary.create(Collections.<String, PerSpanNameSummary>emptyMap());
+
+    @Override
+    public Summary getSummary() {
+      return EMPTY_SUMMARY;
+    }
+
+    @Override
+    public Collection<SpanData> getRunningSpans(Filter filter) {
+      Utils.checkNotNull(filter, "filter");
+      return Collections.<SpanData>emptyList();
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/SampledSpanStore.java b/api/src/main/java/io/opencensus/trace/export/SampledSpanStore.java
new file mode 100644
index 0000000..5d00a45
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/SampledSpanStore.java
@@ -0,0 +1,525 @@
+/*
+ * 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.trace.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Status.CanonicalCode;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * This class allows users to access in-process information such as latency based sampled spans and
+ * error based sampled spans.
+ *
+ * <p>For all completed spans with the option {@link Span.Options#RECORD_EVENTS} the library can
+ * store samples based on latency for succeeded operations or based on error code for failed
+ * operations. To activate this, users MUST manually configure all the span names for which samples
+ * will be collected (see {@link #registerSpanNamesForCollection(Collection)}).
+ *
+ * @since 0.5
+ */
+@ThreadSafe
+public abstract class SampledSpanStore {
+
+  protected SampledSpanStore() {}
+
+  /**
+   * Returns a {@code SampledSpanStore} that maintains a set of span names, but always returns an
+   * empty list of {@link SpanData}.
+   *
+   * @return a {@code SampledSpanStore} that maintains a set of span names, but always returns an
+   *     empty list of {@code SpanData}.
+   */
+  static SampledSpanStore newNoopSampledSpanStore() {
+    return new NoopSampledSpanStore();
+  }
+
+  /**
+   * Returns the summary of all available data, such as number of sampled spans in the latency based
+   * samples or error based samples.
+   *
+   * <p>Data available only for span names registered using {@link
+   * #registerSpanNamesForCollection(Collection)}.
+   *
+   * @return the summary of all available data.
+   * @since 0.5
+   */
+  public abstract Summary getSummary();
+
+  /**
+   * Returns a list of succeeded spans (spans with {@link Status} equal to {@link Status#OK}) that
+   * match the {@code filter}.
+   *
+   * <p>Latency based sampled spans are available only for span names registered using {@link
+   * #registerSpanNamesForCollection(Collection)}.
+   *
+   * @param filter used to filter the returned sampled spans.
+   * @return a list of succeeded spans that match the {@code filter}.
+   * @since 0.5
+   */
+  public abstract Collection<SpanData> getLatencySampledSpans(LatencyFilter filter);
+
+  /**
+   * Returns a list of failed spans (spans with {@link Status} other than {@link Status#OK}) that
+   * match the {@code filter}.
+   *
+   * <p>Error based sampled spans are available only for span names registered using {@link
+   * #registerSpanNamesForCollection(Collection)}.
+   *
+   * @param filter used to filter the returned sampled spans.
+   * @return a list of failed spans that match the {@code filter}.
+   * @since 0.5
+   */
+  public abstract Collection<SpanData> getErrorSampledSpans(ErrorFilter filter);
+
+  /**
+   * Appends a list of span names for which the library will collect latency based sampled spans and
+   * error based sampled spans.
+   *
+   * <p>If called multiple times the library keeps the list of unique span names from all the calls.
+   *
+   * @param spanNames list of span names for which the library will collect samples.
+   * @since 0.5
+   */
+  public abstract void registerSpanNamesForCollection(Collection<String> spanNames);
+
+  /**
+   * Removes a list of span names for which the library will collect latency based sampled spans and
+   * error based sampled spans.
+   *
+   * <p>The library keeps the list of unique registered span names for which samples will be called.
+   * This method allows users to remove span names from that list.
+   *
+   * @param spanNames list of span names for which the library will no longer collect samples.
+   * @since 0.5
+   */
+  public abstract void unregisterSpanNamesForCollection(Collection<String> spanNames);
+
+  /**
+   * Returns the set of unique span names registered to the library, for use in tests. For this set
+   * of span names the library will collect latency based sampled spans and error based sampled
+   * spans.
+   *
+   * <p>This method is only meant for testing code that uses OpenCensus, and it is not performant.
+   *
+   * @return the set of unique span names registered to the library.
+   * @since 0.7
+   */
+  public abstract Set<String> getRegisteredSpanNamesForCollection();
+
+  /**
+   * The summary of all available data.
+   *
+   * @since 0.5
+   */
+  @AutoValue
+  @Immutable
+  public abstract static class Summary {
+
+    Summary() {}
+
+    /**
+     * Returns a new instance of {@code Summary}.
+     *
+     * @param perSpanNameSummary a map with summary for each span name.
+     * @return a new instance of {@code Summary}.
+     * @throws NullPointerException if {@code perSpanNameSummary} is {@code null}.
+     * @since 0.5
+     */
+    public static Summary create(Map<String, PerSpanNameSummary> perSpanNameSummary) {
+      return new AutoValue_SampledSpanStore_Summary(
+          Collections.unmodifiableMap(
+              new HashMap<String, PerSpanNameSummary>(
+                  Utils.checkNotNull(perSpanNameSummary, "perSpanNameSummary"))));
+    }
+
+    /**
+     * Returns a map with summary of available data for each span name.
+     *
+     * @return a map with all the span names and the summary.
+     * @since 0.5
+     */
+    public abstract Map<String, PerSpanNameSummary> getPerSpanNameSummary();
+  }
+
+  /**
+   * Summary of all available data for a span name.
+   *
+   * @since 0.5
+   */
+  @AutoValue
+  @Immutable
+  public abstract static class PerSpanNameSummary {
+
+    PerSpanNameSummary() {}
+
+    /**
+     * Returns a new instance of {@code PerSpanNameSummary}.
+     *
+     * @param numbersOfLatencySampledSpans the summary for the latency buckets.
+     * @param numbersOfErrorSampledSpans the summary for the error buckets.
+     * @return a new instance of {@code PerSpanNameSummary}.
+     * @throws NullPointerException if {@code numbersOfLatencySampledSpans} or {@code
+     *     numbersOfErrorSampledSpans} are {@code null}.
+     * @since 0.5
+     */
+    public static PerSpanNameSummary create(
+        Map<LatencyBucketBoundaries, Integer> numbersOfLatencySampledSpans,
+        Map<CanonicalCode, Integer> numbersOfErrorSampledSpans) {
+      return new AutoValue_SampledSpanStore_PerSpanNameSummary(
+          Collections.unmodifiableMap(
+              new HashMap<LatencyBucketBoundaries, Integer>(
+                  Utils.checkNotNull(
+                      numbersOfLatencySampledSpans, "numbersOfLatencySampledSpans"))),
+          Collections.unmodifiableMap(
+              new HashMap<CanonicalCode, Integer>(
+                  Utils.checkNotNull(numbersOfErrorSampledSpans, "numbersOfErrorSampledSpans"))));
+    }
+
+    /**
+     * Returns the number of sampled spans in all the latency buckets.
+     *
+     * <p>Data available only for span names registered using {@link
+     * #registerSpanNamesForCollection(Collection)}.
+     *
+     * @return the number of sampled spans in all the latency buckets.
+     * @since 0.5
+     */
+    public abstract Map<LatencyBucketBoundaries, Integer> getNumbersOfLatencySampledSpans();
+
+    /**
+     * Returns the number of sampled spans in all the error buckets.
+     *
+     * <p>Data available only for span names registered using {@link
+     * #registerSpanNamesForCollection(Collection)}.
+     *
+     * @return the number of sampled spans in all the error buckets.
+     * @since 0.5
+     */
+    public abstract Map<CanonicalCode, Integer> getNumbersOfErrorSampledSpans();
+  }
+
+  /**
+   * The latency buckets boundaries. Samples based on latency for successful spans (the status of
+   * the span has a canonical code equal to {@link CanonicalCode#OK}) are collected in one of these
+   * latency buckets.
+   *
+   * @since 0.5
+   */
+  public enum LatencyBucketBoundaries {
+    /**
+     * Stores finished successful requests of duration within the interval [0, 10us).
+     *
+     * @since 0.5
+     */
+    ZERO_MICROSx10(0, TimeUnit.MICROSECONDS.toNanos(10)),
+
+    /**
+     * Stores finished successful requests of duration within the interval [10us, 100us).
+     *
+     * @since 0.5
+     */
+    MICROSx10_MICROSx100(TimeUnit.MICROSECONDS.toNanos(10), TimeUnit.MICROSECONDS.toNanos(100)),
+
+    /**
+     * Stores finished successful requests of duration within the interval [100us, 1ms).
+     *
+     * @since 0.5
+     */
+    MICROSx100_MILLIx1(TimeUnit.MICROSECONDS.toNanos(100), TimeUnit.MILLISECONDS.toNanos(1)),
+
+    /**
+     * Stores finished successful requests of duration within the interval [1ms, 10ms).
+     *
+     * @since 0.5
+     */
+    MILLIx1_MILLIx10(TimeUnit.MILLISECONDS.toNanos(1), TimeUnit.MILLISECONDS.toNanos(10)),
+
+    /**
+     * Stores finished successful requests of duration within the interval [10ms, 100ms).
+     *
+     * @since 0.5
+     */
+    MILLIx10_MILLIx100(TimeUnit.MILLISECONDS.toNanos(10), TimeUnit.MILLISECONDS.toNanos(100)),
+
+    /**
+     * Stores finished successful requests of duration within the interval [100ms, 1sec).
+     *
+     * @since 0.5
+     */
+    MILLIx100_SECONDx1(TimeUnit.MILLISECONDS.toNanos(100), TimeUnit.SECONDS.toNanos(1)),
+
+    /**
+     * Stores finished successful requests of duration within the interval [1sec, 10sec).
+     *
+     * @since 0.5
+     */
+    SECONDx1_SECONDx10(TimeUnit.SECONDS.toNanos(1), TimeUnit.SECONDS.toNanos(10)),
+
+    /**
+     * Stores finished successful requests of duration within the interval [10sec, 100sec).
+     *
+     * @since 0.5
+     */
+    SECONDx10_SECONDx100(TimeUnit.SECONDS.toNanos(10), TimeUnit.SECONDS.toNanos(100)),
+
+    /**
+     * Stores finished successful requests of duration &gt;= 100sec.
+     *
+     * @since 0.5
+     */
+    SECONDx100_MAX(TimeUnit.SECONDS.toNanos(100), Long.MAX_VALUE);
+
+    /**
+     * Constructs a {@code LatencyBucketBoundaries} with the given boundaries and label.
+     *
+     * @param latencyLowerNs the latency lower bound of the bucket.
+     * @param latencyUpperNs the latency upper bound of the bucket.
+     */
+    LatencyBucketBoundaries(long latencyLowerNs, long latencyUpperNs) {
+      this.latencyLowerNs = latencyLowerNs;
+      this.latencyUpperNs = latencyUpperNs;
+    }
+
+    /**
+     * Returns the latency lower bound of the bucket.
+     *
+     * @return the latency lower bound of the bucket.
+     * @since 0.5
+     */
+    public long getLatencyLowerNs() {
+      return latencyLowerNs;
+    }
+
+    /**
+     * Returns the latency upper bound of the bucket.
+     *
+     * @return the latency upper bound of the bucket.
+     * @since 0.5
+     */
+    public long getLatencyUpperNs() {
+      return latencyUpperNs;
+    }
+
+    private final long latencyLowerNs;
+    private final long latencyUpperNs;
+  }
+
+  /**
+   * Filter for latency based sampled spans. Used to filter results returned by the {@link
+   * #getLatencySampledSpans(LatencyFilter)} request.
+   *
+   * @since 0.5
+   */
+  @AutoValue
+  @Immutable
+  public abstract static class LatencyFilter {
+
+    LatencyFilter() {}
+
+    /**
+     * Returns a new instance of {@code LatencyFilter}.
+     *
+     * <p>Filters all the spans based on {@code spanName} and latency in the interval
+     * [latencyLowerNs, latencyUpperNs) and returns a maximum of {@code maxSpansToReturn}.
+     *
+     * @param spanName the name of the span.
+     * @param latencyLowerNs the latency lower bound.
+     * @param latencyUpperNs the latency upper bound.
+     * @param maxSpansToReturn the maximum number of results to be returned. {@code 0} means all.
+     * @return a new instance of {@code LatencyFilter}.
+     * @throws NullPointerException if {@code spanName} is {@code null}.
+     * @throws IllegalArgumentException if {@code maxSpansToReturn} or {@code latencyLowerNs} or
+     *     {@code latencyUpperNs} are negative.
+     * @since 0.5
+     */
+    public static LatencyFilter create(
+        String spanName, long latencyLowerNs, long latencyUpperNs, int maxSpansToReturn) {
+      Utils.checkArgument(maxSpansToReturn >= 0, "Negative maxSpansToReturn.");
+      Utils.checkArgument(latencyLowerNs >= 0, "Negative latencyLowerNs");
+      Utils.checkArgument(latencyUpperNs >= 0, "Negative latencyUpperNs");
+      return new AutoValue_SampledSpanStore_LatencyFilter(
+          spanName, latencyLowerNs, latencyUpperNs, maxSpansToReturn);
+    }
+
+    /**
+     * Returns the span name used by this filter.
+     *
+     * @return the span name used by this filter.
+     * @since 0.5
+     */
+    public abstract String getSpanName();
+
+    /**
+     * Returns the latency lower bound of this bucket (inclusive).
+     *
+     * @return the latency lower bound of this bucket.
+     * @since 0.5
+     */
+    public abstract long getLatencyLowerNs();
+
+    /**
+     * Returns the latency upper bound of this bucket (exclusive).
+     *
+     * @return the latency upper bound of this bucket.
+     * @since 0.5
+     */
+    public abstract long getLatencyUpperNs();
+
+    /**
+     * Returns the maximum number of spans to be returned. {@code 0} means all.
+     *
+     * @return the maximum number of spans to be returned.
+     * @since 0.5
+     */
+    public abstract int getMaxSpansToReturn();
+  }
+
+  /**
+   * Filter for error based sampled spans. Used to filter results returned by the {@link
+   * #getErrorSampledSpans(ErrorFilter)} request.
+   *
+   * @since 0.5
+   */
+  @AutoValue
+  @Immutable
+  public abstract static class ErrorFilter {
+
+    ErrorFilter() {}
+
+    /**
+     * Returns a new instance of {@code ErrorFilter}.
+     *
+     * <p>Filters all the spans based on {@code spanName} and {@code canonicalCode} and returns a
+     * maximum of {@code maxSpansToReturn}.
+     *
+     * @param spanName the name of the span.
+     * @param canonicalCode the error code of the span. {@code null} can be used to query all error
+     *     codes.
+     * @param maxSpansToReturn the maximum number of results to be returned. {@code 0} means all.
+     * @return a new instance of {@code ErrorFilter}.
+     * @throws NullPointerException if {@code spanName} is {@code null}.
+     * @throws IllegalArgumentException if {@code canonicalCode} is {@link CanonicalCode#OK} or
+     *     {@code maxSpansToReturn} is negative.
+     * @since 0.5
+     */
+    public static ErrorFilter create(
+        String spanName, @Nullable CanonicalCode canonicalCode, int maxSpansToReturn) {
+      if (canonicalCode != null) {
+        Utils.checkArgument(canonicalCode != CanonicalCode.OK, "Invalid canonical code.");
+      }
+      Utils.checkArgument(maxSpansToReturn >= 0, "Negative maxSpansToReturn.");
+      return new AutoValue_SampledSpanStore_ErrorFilter(spanName, canonicalCode, maxSpansToReturn);
+    }
+
+    /**
+     * Returns the span name used by this filter.
+     *
+     * @return the span name used by this filter.
+     * @since 0.5
+     */
+    public abstract String getSpanName();
+
+    /**
+     * Returns the canonical code used by this filter. Always different than {@link
+     * CanonicalCode#OK}. If {@code null} then all errors match.
+     *
+     * @return the canonical code used by this filter.
+     * @since 0.5
+     */
+    @Nullable
+    public abstract CanonicalCode getCanonicalCode();
+
+    /**
+     * Returns the maximum number of spans to be returned. Used to enforce the number of returned
+     * {@code SpanData}. {@code 0} means all.
+     *
+     * @return the maximum number of spans to be returned.
+     * @since 0.5
+     */
+    public abstract int getMaxSpansToReturn();
+  }
+
+  @ThreadSafe
+  private static final class NoopSampledSpanStore extends SampledSpanStore {
+    private static final PerSpanNameSummary EMPTY_PER_SPAN_NAME_SUMMARY =
+        PerSpanNameSummary.create(
+            Collections.<SampledSpanStore.LatencyBucketBoundaries, Integer>emptyMap(),
+            Collections.<CanonicalCode, Integer>emptyMap());
+
+    @GuardedBy("registeredSpanNames")
+    private final Set<String> registeredSpanNames = new HashSet<String>();
+
+    @Override
+    public Summary getSummary() {
+      Map<String, PerSpanNameSummary> result = new HashMap<String, PerSpanNameSummary>();
+      synchronized (registeredSpanNames) {
+        for (String registeredSpanName : registeredSpanNames) {
+          result.put(registeredSpanName, EMPTY_PER_SPAN_NAME_SUMMARY);
+        }
+      }
+      return Summary.create(result);
+    }
+
+    @Override
+    public Collection<SpanData> getLatencySampledSpans(LatencyFilter filter) {
+      Utils.checkNotNull(filter, "latencyFilter");
+      return Collections.<SpanData>emptyList();
+    }
+
+    @Override
+    public Collection<SpanData> getErrorSampledSpans(ErrorFilter filter) {
+      Utils.checkNotNull(filter, "errorFilter");
+      return Collections.<SpanData>emptyList();
+    }
+
+    @Override
+    public void registerSpanNamesForCollection(Collection<String> spanNames) {
+      Utils.checkNotNull(spanNames, "spanNames");
+      synchronized (registeredSpanNames) {
+        registeredSpanNames.addAll(spanNames);
+      }
+    }
+
+    @Override
+    public void unregisterSpanNamesForCollection(Collection<String> spanNames) {
+      Utils.checkNotNull(spanNames, "spanNames");
+      synchronized (registeredSpanNames) {
+        registeredSpanNames.removeAll(spanNames);
+      }
+    }
+
+    @Override
+    public Set<String> getRegisteredSpanNamesForCollection() {
+      synchronized (registeredSpanNames) {
+        return Collections.<String>unmodifiableSet(new HashSet<String>(registeredSpanNames));
+      }
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/SpanData.java b/api/src/main/java/io/opencensus/trace/export/SpanData.java
new file mode 100644
index 0000000..f4dd468
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/SpanData.java
@@ -0,0 +1,477 @@
+/*
+ * 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.trace.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.internal.BaseMessageEventUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/*>>>
+import org.checkerframework.dataflow.qual.Deterministic;
+*/
+
+/**
+ * Immutable representation of all data collected by the {@link Span} class.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class SpanData {
+
+  /**
+   * Returns a new immutable {@code SpanData}.
+   *
+   * @deprecated Use {@link #create(SpanContext, SpanId, Boolean, String, Kind, Timestamp,
+   *     Attributes, TimedEvents, TimedEvents, Links, Integer, Status, Timestamp)}.
+   */
+  @Deprecated
+  public static SpanData create(
+      SpanContext context,
+      @Nullable SpanId parentSpanId,
+      @Nullable Boolean hasRemoteParent,
+      String name,
+      Timestamp startTimestamp,
+      Attributes attributes,
+      TimedEvents<Annotation> annotations,
+      TimedEvents<? extends io.opencensus.trace.BaseMessageEvent> messageOrNetworkEvents,
+      Links links,
+      @Nullable Integer childSpanCount,
+      @Nullable Status status,
+      @Nullable Timestamp endTimestamp) {
+    return create(
+        context,
+        parentSpanId,
+        hasRemoteParent,
+        name,
+        null,
+        startTimestamp,
+        attributes,
+        annotations,
+        messageOrNetworkEvents,
+        links,
+        childSpanCount,
+        status,
+        endTimestamp);
+  }
+
+  /**
+   * Returns a new immutable {@code SpanData}.
+   *
+   * @param context the {@code SpanContext} of the {@code Span}.
+   * @param parentSpanId the parent {@code SpanId} of the {@code Span}. {@code null} if the {@code
+   *     Span} is a root.
+   * @param hasRemoteParent {@code true} if the parent {@code Span} is remote. {@code null} if this
+   *     is a root span.
+   * @param name the name of the {@code Span}.
+   * @param kind the kind of the {@code Span}.
+   * @param startTimestamp the start {@code Timestamp} of the {@code Span}.
+   * @param attributes the attributes associated with the {@code Span}.
+   * @param annotations the annotations associated with the {@code Span}.
+   * @param messageOrNetworkEvents the message events (or network events for backward compatibility)
+   *     associated with the {@code Span}.
+   * @param links the links associated with the {@code Span}.
+   * @param childSpanCount the number of child spans that were generated while the span was active.
+   * @param status the {@code Status} of the {@code Span}. {@code null} if the {@code Span} is still
+   *     active.
+   * @param endTimestamp the end {@code Timestamp} of the {@code Span}. {@code null} if the {@code
+   *     Span} is still active.
+   * @return a new immutable {@code SpanData}.
+   * @since 0.14
+   */
+  @SuppressWarnings({"deprecation", "InconsistentOverloads"})
+  public static SpanData create(
+      SpanContext context,
+      @Nullable SpanId parentSpanId,
+      @Nullable Boolean hasRemoteParent,
+      String name,
+      @Nullable Kind kind,
+      Timestamp startTimestamp,
+      Attributes attributes,
+      TimedEvents<Annotation> annotations,
+      TimedEvents<? extends io.opencensus.trace.BaseMessageEvent> messageOrNetworkEvents,
+      Links links,
+      @Nullable Integer childSpanCount,
+      @Nullable Status status,
+      @Nullable Timestamp endTimestamp) {
+    Utils.checkNotNull(messageOrNetworkEvents, "messageOrNetworkEvents");
+    List<TimedEvent<MessageEvent>> messageEventsList = new ArrayList<TimedEvent<MessageEvent>>();
+    for (TimedEvent<? extends io.opencensus.trace.BaseMessageEvent> timedEvent :
+        messageOrNetworkEvents.getEvents()) {
+      io.opencensus.trace.BaseMessageEvent event = timedEvent.getEvent();
+      if (event instanceof MessageEvent) {
+        @SuppressWarnings("unchecked")
+        TimedEvent<MessageEvent> timedMessageEvent = (TimedEvent<MessageEvent>) timedEvent;
+        messageEventsList.add(timedMessageEvent);
+      } else {
+        messageEventsList.add(
+            TimedEvent.<MessageEvent>create(
+                timedEvent.getTimestamp(), BaseMessageEventUtils.asMessageEvent(event)));
+      }
+    }
+    TimedEvents<MessageEvent> messageEvents =
+        TimedEvents.<MessageEvent>create(
+            messageEventsList, messageOrNetworkEvents.getDroppedEventsCount());
+    return new AutoValue_SpanData(
+        context,
+        parentSpanId,
+        hasRemoteParent,
+        name,
+        kind,
+        startTimestamp,
+        attributes,
+        annotations,
+        messageEvents,
+        links,
+        childSpanCount,
+        status,
+        endTimestamp);
+  }
+
+  /**
+   * Returns the {@code SpanContext} associated with this {@code Span}.
+   *
+   * @return the {@code SpanContext} associated with this {@code Span}.
+   * @since 0.5
+   */
+  public abstract SpanContext getContext();
+
+  /**
+   * Returns the parent {@code SpanId} or {@code null} if the {@code Span} is a root {@code Span}.
+   *
+   * @return the parent {@code SpanId} or {@code null} if the {@code Span} is a root {@code Span}.
+   * @since 0.5
+   */
+  @Nullable
+  /*@Deterministic*/
+  public abstract SpanId getParentSpanId();
+
+  /**
+   * Returns {@code true} if the parent is on a different process. {@code null} if this is a root
+   * span.
+   *
+   * @return {@code true} if the parent is on a different process. {@code null} if this is a root
+   *     span.
+   * @since 0.5
+   */
+  @Nullable
+  public abstract Boolean getHasRemoteParent();
+
+  /**
+   * Returns the name of this {@code Span}.
+   *
+   * @return the name of this {@code Span}.
+   * @since 0.5
+   */
+  public abstract String getName();
+
+  /**
+   * Returns the kind of this {@code Span}.
+   *
+   * @return the kind of this {@code Span}.
+   * @since 0.14
+   */
+  @Nullable
+  public abstract Kind getKind();
+
+  /**
+   * Returns the start {@code Timestamp} of this {@code Span}.
+   *
+   * @return the start {@code Timestamp} of this {@code Span}.
+   * @since 0.5
+   */
+  public abstract Timestamp getStartTimestamp();
+
+  /**
+   * Returns the attributes recorded for this {@code Span}.
+   *
+   * @return the attributes recorded for this {@code Span}.
+   * @since 0.5
+   */
+  public abstract Attributes getAttributes();
+
+  /**
+   * Returns the annotations recorded for this {@code Span}.
+   *
+   * @return the annotations recorded for this {@code Span}.
+   * @since 0.5
+   */
+  public abstract TimedEvents<Annotation> getAnnotations();
+
+  /**
+   * Returns network events recorded for this {@code Span}.
+   *
+   * @return network events recorded for this {@code Span}.
+   * @deprecated Use {@link #getMessageEvents}.
+   * @since 0.5
+   */
+  @Deprecated
+  @SuppressWarnings({"deprecation"})
+  public TimedEvents<io.opencensus.trace.NetworkEvent> getNetworkEvents() {
+    TimedEvents<MessageEvent> timedEvents = getMessageEvents();
+    List<TimedEvent<io.opencensus.trace.NetworkEvent>> networkEventsList =
+        new ArrayList<TimedEvent<io.opencensus.trace.NetworkEvent>>();
+    for (TimedEvent<MessageEvent> timedEvent : timedEvents.getEvents()) {
+      networkEventsList.add(
+          TimedEvent.<io.opencensus.trace.NetworkEvent>create(
+              timedEvent.getTimestamp(),
+              BaseMessageEventUtils.asNetworkEvent(timedEvent.getEvent())));
+    }
+    return TimedEvents.<io.opencensus.trace.NetworkEvent>create(
+        networkEventsList, timedEvents.getDroppedEventsCount());
+  }
+
+  /**
+   * Returns message events recorded for this {@code Span}.
+   *
+   * @return message events recorded for this {@code Span}.
+   * @since 0.12
+   */
+  public abstract TimedEvents<MessageEvent> getMessageEvents();
+
+  /**
+   * Returns links recorded for this {@code Span}.
+   *
+   * @return links recorded for this {@code Span}.
+   * @since 0.5
+   */
+  public abstract Links getLinks();
+
+  /**
+   * Returns the number of child spans that were generated while the {@code Span} was running. If
+   * not {@code null} allows service implementations to detect missing child spans.
+   *
+   * <p>This information is not always available.
+   *
+   * @return the number of child spans that were generated while the {@code Span} was running.
+   * @since 0.5
+   */
+  @Nullable
+  public abstract Integer getChildSpanCount();
+
+  /**
+   * Returns the {@code Status} or {@code null} if {@code Span} is still active.
+   *
+   * @return the {@code Status} or {@code null} if {@code Span} is still active.
+   * @since 0.5
+   */
+  @Nullable
+  /*@Deterministic*/
+  public abstract Status getStatus();
+
+  /**
+   * Returns the end {@code Timestamp} or {@code null} if the {@code Span} is still active.
+   *
+   * @return the end {@code Timestamp} or {@code null} if the {@code Span} is still active.
+   * @since 0.5
+   */
+  @Nullable
+  /*@Deterministic*/
+  public abstract Timestamp getEndTimestamp();
+
+  SpanData() {}
+
+  /**
+   * A timed event representation.
+   *
+   * @param <T> the type of value that is timed.
+   * @since 0.5
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class TimedEvent<T> {
+    /**
+     * Returns a new immutable {@code TimedEvent<T>}.
+     *
+     * @param timestamp the {@code Timestamp} of this event.
+     * @param event the event.
+     * @param <T> the type of value that is timed.
+     * @return a new immutable {@code TimedEvent<T>}
+     * @since 0.5
+     */
+    public static <T> TimedEvent<T> create(Timestamp timestamp, T event) {
+      return new AutoValue_SpanData_TimedEvent<T>(timestamp, event);
+    }
+
+    /**
+     * Returns the {@code Timestamp} of this event.
+     *
+     * @return the {@code Timestamp} of this event.
+     * @since 0.5
+     */
+    public abstract Timestamp getTimestamp();
+
+    /**
+     * Returns the event.
+     *
+     * @return the event.
+     * @since 0.5
+     */
+    /*@Deterministic*/
+    public abstract T getEvent();
+
+    TimedEvent() {}
+  }
+
+  /**
+   * A list of timed events and the number of dropped events representation.
+   *
+   * @param <T> the type of value that is timed.
+   * @since 0.5
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class TimedEvents<T> {
+    /**
+     * Returns a new immutable {@code TimedEvents<T>}.
+     *
+     * @param events the list of events.
+     * @param droppedEventsCount the number of dropped events.
+     * @param <T> the type of value that is timed.
+     * @return a new immutable {@code TimedEvents<T>}
+     * @since 0.5
+     */
+    public static <T> TimedEvents<T> create(List<TimedEvent<T>> events, int droppedEventsCount) {
+      return new AutoValue_SpanData_TimedEvents<T>(
+          Collections.unmodifiableList(
+              new ArrayList<TimedEvent<T>>(Utils.checkNotNull(events, "events"))),
+          droppedEventsCount);
+    }
+
+    /**
+     * Returns the list of events.
+     *
+     * @return the list of events.
+     * @since 0.5
+     */
+    public abstract List<TimedEvent<T>> getEvents();
+
+    /**
+     * Returns the number of dropped events.
+     *
+     * @return the number of dropped events.
+     * @since 0.5
+     */
+    public abstract int getDroppedEventsCount();
+
+    TimedEvents() {}
+  }
+
+  /**
+   * A set of attributes and the number of dropped attributes representation.
+   *
+   * @since 0.5
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class Attributes {
+    /**
+     * Returns a new immutable {@code Attributes}.
+     *
+     * @param attributeMap the set of attributes.
+     * @param droppedAttributesCount the number of dropped attributes.
+     * @return a new immutable {@code Attributes}.
+     * @since 0.5
+     */
+    public static Attributes create(
+        Map<String, AttributeValue> attributeMap, int droppedAttributesCount) {
+      // TODO(bdrutu): Consider to use LinkedHashMap here and everywhere else, less test flakes
+      // for others on account of determinism.
+      return new AutoValue_SpanData_Attributes(
+          Collections.unmodifiableMap(
+              new HashMap<String, AttributeValue>(
+                  Utils.checkNotNull(attributeMap, "attributeMap"))),
+          droppedAttributesCount);
+    }
+
+    /**
+     * Returns the set of attributes.
+     *
+     * @return the set of attributes.
+     * @since 0.5
+     */
+    public abstract Map<String, AttributeValue> getAttributeMap();
+
+    /**
+     * Returns the number of dropped attributes.
+     *
+     * @return the number of dropped attributes.
+     * @since 0.5
+     */
+    public abstract int getDroppedAttributesCount();
+
+    Attributes() {}
+  }
+
+  /**
+   * A list of links and the number of dropped links representation.
+   *
+   * @since 0.5
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class Links {
+    /**
+     * Returns a new immutable {@code Links}.
+     *
+     * @param links the list of links.
+     * @param droppedLinksCount the number of dropped links.
+     * @return a new immutable {@code Links}.
+     * @since 0.5
+     */
+    public static Links create(List<Link> links, int droppedLinksCount) {
+      return new AutoValue_SpanData_Links(
+          Collections.unmodifiableList(new ArrayList<Link>(Utils.checkNotNull(links, "links"))),
+          droppedLinksCount);
+    }
+
+    /**
+     * Returns the list of links.
+     *
+     * @return the list of links.
+     * @since 0.5
+     */
+    public abstract List<Link> getLinks();
+
+    /**
+     * Returns the number of dropped links.
+     *
+     * @return the number of dropped links.
+     * @since 0.5
+     */
+    public abstract int getDroppedLinksCount();
+
+    Links() {}
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/SpanExporter.java b/api/src/main/java/io/opencensus/trace/export/SpanExporter.java
new file mode 100644
index 0000000..73ac526
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/SpanExporter.java
@@ -0,0 +1,96 @@
+/*
+ * 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.trace.export;
+
+import io.opencensus.trace.Span;
+import io.opencensus.trace.TraceOptions;
+import java.util.Collection;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A service that is used by the library to export {@code SpanData} for all the spans that are part
+ * of a distributed sampled trace (see {@link TraceOptions#isSampled()}).
+ *
+ * @since 0.5
+ */
+@ThreadSafe
+public abstract class SpanExporter {
+  private static final SpanExporter NOOP_SPAN_EXPORTER = new NoopSpanExporter();
+
+  /**
+   * Returns the no-op implementation of the {@code ExportComponent}.
+   *
+   * @return the no-op implementation of the {@code ExportComponent}.
+   * @since 0.5
+   */
+  public static SpanExporter getNoopSpanExporter() {
+    return NOOP_SPAN_EXPORTER;
+  }
+
+  /**
+   * Registers a new service handler that is used by the library to export {@code SpanData} for
+   * sampled spans (see {@link TraceOptions#isSampled()}).
+   *
+   * @param name the name of the service handler. Must be unique for each service.
+   * @param handler the service handler that is called for each ended sampled span.
+   * @since 0.5
+   */
+  public abstract void registerHandler(String name, Handler handler);
+
+  /**
+   * Unregisters the service handler with the provided name.
+   *
+   * @param name the name of the service handler that will be unregistered.
+   * @since 0.5
+   */
+  public abstract void unregisterHandler(String name);
+
+  /**
+   * An abstract class that allows different tracing services to export recorded data for sampled
+   * spans in their own format.
+   *
+   * <p>To export data this MUST be register to to the ExportComponent using {@link
+   * #registerHandler(String, Handler)}.
+   *
+   * @since 0.5
+   */
+  public abstract static class Handler {
+
+    /**
+     * Exports a list of sampled (see {@link TraceOptions#isSampled()}) {@link Span}s using the
+     * immutable representation {@link SpanData}.
+     *
+     * <p>This may be called from a different thread than the one that called {@link Span#end()}.
+     *
+     * <p>Implementation SHOULD not block the calling thread. It should execute the export on a
+     * different thread if possible.
+     *
+     * @param spanDataList a list of {@code SpanData} objects to be exported.
+     * @since 0.5
+     */
+    public abstract void export(Collection<SpanData> spanDataList);
+  }
+
+  private static final class NoopSpanExporter extends SpanExporter {
+
+    @Override
+    public void registerHandler(String name, Handler handler) {}
+
+    @Override
+    public void unregisterHandler(String name) {}
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/internal/BaseMessageEventUtils.java b/api/src/main/java/io/opencensus/trace/internal/BaseMessageEventUtils.java
new file mode 100644
index 0000000..9d22a1c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/internal/BaseMessageEventUtils.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018, 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.trace.internal;
+
+import io.opencensus.common.Internal;
+import io.opencensus.internal.Utils;
+
+/**
+ * Helper class to convert/cast between for {@link io.opencensus.trace.MessageEvent} and {@link
+ * io.opencensus.trace.NetworkEvent}.
+ */
+@Internal
+@SuppressWarnings("deprecation")
+public final class BaseMessageEventUtils {
+  /**
+   * Cast or convert a {@link io.opencensus.trace.BaseMessageEvent} to {@link
+   * io.opencensus.trace.MessageEvent}.
+   *
+   * <p>Warning: if the input is a {@code io.opencensus.trace.NetworkEvent} and contains {@code
+   * kernelTimestamp} information, this information will be dropped.
+   *
+   * @param event the {@code BaseMessageEvent} that is being cast or converted.
+   * @return a {@code MessageEvent} representation of the input.
+   */
+  public static io.opencensus.trace.MessageEvent asMessageEvent(
+      io.opencensus.trace.BaseMessageEvent event) {
+    Utils.checkNotNull(event, "event");
+    if (event instanceof io.opencensus.trace.MessageEvent) {
+      return (io.opencensus.trace.MessageEvent) event;
+    }
+    io.opencensus.trace.NetworkEvent networkEvent = (io.opencensus.trace.NetworkEvent) event;
+    io.opencensus.trace.MessageEvent.Type type =
+        (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.RECV)
+            ? io.opencensus.trace.MessageEvent.Type.RECEIVED
+            : io.opencensus.trace.MessageEvent.Type.SENT;
+    return io.opencensus.trace.MessageEvent.builder(type, networkEvent.getMessageId())
+        .setUncompressedMessageSize(networkEvent.getUncompressedMessageSize())
+        .setCompressedMessageSize(networkEvent.getCompressedMessageSize())
+        .build();
+  }
+
+  /**
+   * Cast or convert a {@link io.opencensus.trace.BaseMessageEvent} to {@link
+   * io.opencensus.trace.NetworkEvent}.
+   *
+   * @param event the {@code BaseMessageEvent} that is being cast or converted.
+   * @return a {@code io.opencensus.trace.NetworkEvent} representation of the input.
+   */
+  public static io.opencensus.trace.NetworkEvent asNetworkEvent(
+      io.opencensus.trace.BaseMessageEvent event) {
+    Utils.checkNotNull(event, "event");
+    if (event instanceof io.opencensus.trace.NetworkEvent) {
+      return (io.opencensus.trace.NetworkEvent) event;
+    }
+    io.opencensus.trace.MessageEvent messageEvent = (io.opencensus.trace.MessageEvent) event;
+    io.opencensus.trace.NetworkEvent.Type type =
+        (messageEvent.getType() == io.opencensus.trace.MessageEvent.Type.RECEIVED)
+            ? io.opencensus.trace.NetworkEvent.Type.RECV
+            : io.opencensus.trace.NetworkEvent.Type.SENT;
+    return io.opencensus.trace.NetworkEvent.builder(type, messageEvent.getMessageId())
+        .setUncompressedMessageSize(messageEvent.getUncompressedMessageSize())
+        .setCompressedMessageSize(messageEvent.getCompressedMessageSize())
+        .build();
+  }
+
+  private BaseMessageEventUtils() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/package-info.java b/api/src/main/java/io/opencensus/trace/package-info.java
new file mode 100644
index 0000000..77f39ab
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/package-info.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/**
+ * API for distributed tracing.
+ *
+ * <p>Distributed tracing, also called distributed request tracing, is a technique that helps
+ * debugging distributed applications.
+ *
+ * <p>Trace represents a tree of spans. A trace has a root span that encapsulates all the spans from
+ * start to end, and the children spans being the distinct calls invoked in between.
+ *
+ * <p>{@link io.opencensus.trace.Span} represents a single operation within a trace.
+ *
+ * <p>{@link io.opencensus.trace.Span Spans} are propagated in-process in the {@code
+ * io.grpc.Context} and between process using one of the wire propagation formats supported in the
+ * {@code io.opencensus.trace.propagation} package.
+ */
+// TODO: Add code examples.
+package io.opencensus.trace;
diff --git a/api/src/main/java/io/opencensus/trace/propagation/BinaryFormat.java b/api/src/main/java/io/opencensus/trace/propagation/BinaryFormat.java
new file mode 100644
index 0000000..7e875fd
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/propagation/BinaryFormat.java
@@ -0,0 +1,156 @@
+/*
+ * 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.trace.propagation;
+
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.SpanContext;
+import java.text.ParseException;
+
+/**
+ * This is a helper class for {@link SpanContext} propagation on the wire using binary encoding.
+ *
+ * <p>Example of usage on the client:
+ *
+ * <pre>{@code
+ * private static final Tracer tracer = Tracing.getTracer();
+ * private static final BinaryFormat binaryFormat =
+ *     Tracing.getPropagationComponent().getBinaryFormat();
+ * void onSendRequest() {
+ *   try (Scope ss = tracer.spanBuilder("Sent.MyRequest").startScopedSpan()) {
+ *     byte[] binaryValue = binaryFormat.toByteArray(tracer.getCurrentContext().context());
+ *     // Send the request including the binaryValue and wait for the response.
+ *   }
+ * }
+ * }</pre>
+ *
+ * <p>Example of usage on the server:
+ *
+ * <pre>{@code
+ * private static final Tracer tracer = Tracing.getTracer();
+ * private static final BinaryFormat binaryFormat =
+ *     Tracing.getPropagationComponent().getBinaryFormat();
+ * void onRequestReceived() {
+ *   // Get the binaryValue from the request.
+ *   SpanContext spanContext = SpanContext.INVALID;
+ *   try {
+ *     if (binaryValue != null) {
+ *       spanContext = binaryFormat.fromByteArray(binaryValue);
+ *     }
+ *   } catch (SpanContextParseException e) {
+ *     // Maybe log the exception.
+ *   }
+ *   try (Scope ss =
+ *            tracer.spanBuilderWithRemoteParent("Recv.MyRequest", spanContext).startScopedSpan()) {
+ *     // Handle request and send response back.
+ *   }
+ * }
+ * }</pre>
+ *
+ * @since 0.5
+ */
+public abstract class BinaryFormat {
+  static final NoopBinaryFormat NOOP_BINARY_FORMAT = new NoopBinaryFormat();
+
+  /**
+   * Serializes a {@link SpanContext} into a byte array using the binary format.
+   *
+   * @deprecated use {@link #toByteArray(SpanContext)}.
+   * @param spanContext the {@code SpanContext} to serialize.
+   * @return the serialized binary value.
+   * @throws NullPointerException if the {@code spanContext} is {@code null}.
+   * @since 0.5
+   */
+  @Deprecated
+  public byte[] toBinaryValue(SpanContext spanContext) {
+    return toByteArray(spanContext);
+  }
+
+  /**
+   * Serializes a {@link SpanContext} into a byte array using the binary format.
+   *
+   * @param spanContext the {@code SpanContext} to serialize.
+   * @return the serialized binary value.
+   * @throws NullPointerException if the {@code spanContext} is {@code null}.
+   * @since 0.7
+   */
+  public byte[] toByteArray(SpanContext spanContext) {
+    // Implementation must override this method.
+    return toBinaryValue(spanContext);
+  }
+
+  /**
+   * Parses the {@link SpanContext} from a byte array using the binary format.
+   *
+   * @deprecated use {@link #fromByteArray(byte[])}.
+   * @param bytes a binary encoded buffer from which the {@code SpanContext} will be parsed.
+   * @return the parsed {@code SpanContext}.
+   * @throws NullPointerException if the {@code input} is {@code null}.
+   * @throws ParseException if the version is not supported or the input is invalid
+   * @since 0.5
+   */
+  @Deprecated
+  public SpanContext fromBinaryValue(byte[] bytes) throws ParseException {
+    try {
+      return fromByteArray(bytes);
+    } catch (SpanContextParseException e) {
+      throw new ParseException(e.toString(), 0);
+    }
+  }
+
+  /**
+   * Parses the {@link SpanContext} from a byte array using the binary format.
+   *
+   * @param bytes a binary encoded buffer from which the {@code SpanContext} will be parsed.
+   * @return the parsed {@code SpanContext}.
+   * @throws NullPointerException if the {@code input} is {@code null}.
+   * @throws SpanContextParseException if the version is not supported or the input is invalid
+   * @since 0.7
+   */
+  public SpanContext fromByteArray(byte[] bytes) throws SpanContextParseException {
+    // Implementation must override this method. If it doesn't, the below will StackOverflowError.
+    try {
+      return fromBinaryValue(bytes);
+    } catch (ParseException e) {
+      throw new SpanContextParseException("Error while parsing.", e);
+    }
+  }
+
+  /**
+   * Returns the no-op implementation of the {@code BinaryFormat}.
+   *
+   * @return the no-op implementation of the {@code BinaryFormat}.
+   */
+  static BinaryFormat getNoopBinaryFormat() {
+    return NOOP_BINARY_FORMAT;
+  }
+
+  private static final class NoopBinaryFormat extends BinaryFormat {
+    @Override
+    public byte[] toByteArray(SpanContext spanContext) {
+      Utils.checkNotNull(spanContext, "spanContext");
+      return new byte[0];
+    }
+
+    @Override
+    public SpanContext fromByteArray(byte[] bytes) {
+      Utils.checkNotNull(bytes, "bytes");
+      return SpanContext.INVALID;
+    }
+
+    private NoopBinaryFormat() {}
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/propagation/PropagationComponent.java b/api/src/main/java/io/opencensus/trace/propagation/PropagationComponent.java
new file mode 100644
index 0000000..a90f041
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/propagation/PropagationComponent.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.trace.propagation;
+
+import io.opencensus.common.ExperimentalApi;
+
+/**
+ * Container class for all the supported propagation formats. Currently supports only Binary format
+ * (see {@link BinaryFormat}) and B3 Text format (see {@link TextFormat}) but more formats will be
+ * added.
+ *
+ * @since 0.5
+ */
+public abstract class PropagationComponent {
+  private static final PropagationComponent NOOP_PROPAGATION_COMPONENT =
+      new NoopPropagationComponent();
+
+  /**
+   * Returns the {@link BinaryFormat} with the provided implementations. If no implementation is
+   * provided then no-op implementation will be used.
+   *
+   * @return the {@code BinaryFormat} implementation.
+   * @since 0.5
+   */
+  public abstract BinaryFormat getBinaryFormat();
+
+  /**
+   * Returns the B3 {@link TextFormat} with the provided implementations. See <a
+   * href="https://github.com/openzipkin/b3-propagation">b3-propagation</a> for more information. If
+   * no implementation is provided then no-op implementation will be used.
+   *
+   * @since 0.11.0
+   * @return the B3 {@code TextFormat} implementation for B3.
+   */
+  @ExperimentalApi
+  public abstract TextFormat getB3Format();
+
+  /**
+   * Returns an instance that contains no-op implementations for all the instances.
+   *
+   * @return an instance that contains no-op implementations for all the instances.
+   * @since 0.5
+   */
+  public static PropagationComponent getNoopPropagationComponent() {
+    return NOOP_PROPAGATION_COMPONENT;
+  }
+
+  private static final class NoopPropagationComponent extends PropagationComponent {
+    @Override
+    public BinaryFormat getBinaryFormat() {
+      return BinaryFormat.getNoopBinaryFormat();
+    }
+
+    @Override
+    public TextFormat getB3Format() {
+      return TextFormat.getNoopTextFormat();
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/propagation/SpanContextParseException.java b/api/src/main/java/io/opencensus/trace/propagation/SpanContextParseException.java
new file mode 100644
index 0000000..80d42af
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/propagation/SpanContextParseException.java
@@ -0,0 +1,47 @@
+/*
+ * 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.trace.propagation;
+
+/**
+ * Exception thrown when a {@link io.opencensus.trace.SpanContext} cannot be parsed.
+ *
+ * @since 0.7
+ */
+public final class SpanContextParseException extends Exception {
+  private static final long serialVersionUID = 0L;
+
+  /**
+   * Constructs a new {@code SpanContextParseException} with the given message.
+   *
+   * @param message a message describing the parse error.
+   * @since 0.7
+   */
+  public SpanContextParseException(String message) {
+    super(message);
+  }
+
+  /**
+   * Constructs a new {@code SpanContextParseException} with the given message and cause.
+   *
+   * @param message a message describing the parse error.
+   * @param cause the cause of the parse error.
+   * @since 0.7
+   */
+  public SpanContextParseException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/propagation/TextFormat.java b/api/src/main/java/io/opencensus/trace/propagation/TextFormat.java
new file mode 100644
index 0000000..d52e71f
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/propagation/TextFormat.java
@@ -0,0 +1,204 @@
+/*
+ * 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.trace.propagation;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.SpanContext;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.NonNull;
+*/
+
+/**
+ * Injects and extracts {@link SpanContext trace identifiers} as text into carriers that travel
+ * in-band across process boundaries. Identifiers are often encoded as messaging or RPC request
+ * headers.
+ *
+ * <p>When using http, the carrier of propagated data on both the client (injector) and server
+ * (extractor) side is usually an http request. Propagation is usually implemented via library-
+ * specific request interceptors, where the client-side injects span identifiers and the server-side
+ * extracts them.
+ *
+ * <p>Example of usage on the client:
+ *
+ * <pre>{@code
+ * private static final Tracer tracer = Tracing.getTracer();
+ * private static final TextFormat textFormat = Tracing.getPropagationComponent().getTextFormat();
+ * private static final TextFormat.Setter setter = new TextFormat.Setter<HttpURLConnection>() {
+ *   public void put(HttpURLConnection carrier, String key, String value) {
+ *     carrier.setRequestProperty(field, value);
+ *   }
+ * }
+ *
+ * void makeHttpRequest() {
+ *   Span span = tracer.spanBuilder("Sent.MyRequest").startSpan();
+ *   try (Scope s = tracer.withSpan(span)) {
+ *     HttpURLConnection connection =
+ *         (HttpURLConnection) new URL("http://myserver").openConnection();
+ *     textFormat.inject(span.getContext(), connection, httpURLConnectionSetter);
+ *     // Send the request, wait for response and maybe set the status if not ok.
+ *   }
+ *   span.end();  // Can set a status.
+ * }
+ * }</pre>
+ *
+ * <p>Example of usage on the server:
+ *
+ * <pre>{@code
+ * private static final Tracer tracer = Tracing.getTracer();
+ * private static final TextFormat textFormat = Tracing.getPropagationComponent().getTextFormat();
+ * private static final TextFormat.Getter<HttpRequest> getter = ...;
+ *
+ * void onRequestReceived(HttpRequest request) {
+ *   SpanContext spanContext = textFormat.extract(request, getter);
+ *   Span span = tracer.spanBuilderWithRemoteParent("Recv.MyRequest", spanContext).startSpan();
+ *   try (Scope s = tracer.withSpan(span)) {
+ *     // Handle request and send response back.
+ *   }
+ *   span.end()
+ * }
+ * }</pre>
+ *
+ * @since 0.11
+ */
+@ExperimentalApi
+public abstract class TextFormat {
+  private static final NoopTextFormat NOOP_TEXT_FORMAT = new NoopTextFormat();
+
+  /**
+   * The propagation fields defined. If your carrier is reused, you should delete the fields here
+   * before calling {@link #inject(SpanContext, Object, Setter)}.
+   *
+   * <p>For example, if the carrier is a single-use or immutable request object, you don't need to
+   * clear fields as they couldn't have been set before. If it is a mutable, retryable object,
+   * successive calls should clear these fields first.
+   *
+   * @since 0.11
+   */
+  // The use cases of this are:
+  // * allow pre-allocation of fields, especially in systems like gRPC Metadata
+  // * allow a single-pass over an iterator (ex OpenTracing has no getter in TextMap)
+  public abstract List<String> fields();
+
+  /**
+   * Injects the span context downstream. For example, as http headers.
+   *
+   * @param spanContext possibly not sampled.
+   * @param carrier holds propagation fields. For example, an outgoing message or http request.
+   * @param setter invoked for each propagation key to add or remove.
+   * @since 0.11
+   */
+  public abstract <C /*>>> extends @NonNull Object*/> void inject(
+      SpanContext spanContext, C carrier, Setter<C> setter);
+
+  /**
+   * Class that allows a {@code TextFormat} to set propagated fields into a carrier.
+   *
+   * <p>{@code Setter} is stateless and allows to be saved as a constant to avoid runtime
+   * allocations.
+   *
+   * @param <C> carrier of propagation fields, such as an http request
+   * @since 0.11
+   */
+  public abstract static class Setter<C> {
+
+    /**
+     * Replaces a propagated field with the given value.
+     *
+     * <p>For example, a setter for an {@link java.net.HttpURLConnection} would be the method
+     * reference {@link java.net.HttpURLConnection#addRequestProperty(String, String)}
+     *
+     * @param carrier holds propagation fields. For example, an outgoing message or http request.
+     * @param key the key of the field.
+     * @param value the value of the field.
+     * @since 0.11
+     */
+    public abstract void put(C carrier, String key, String value);
+  }
+
+  /**
+   * Extracts the span context from upstream. For example, as http headers.
+   *
+   * @param carrier holds propagation fields. For example, an outgoing message or http request.
+   * @param getter invoked for each propagation key to get.
+   * @throws SpanContextParseException if the input is invalid
+   * @since 0.11
+   */
+  public abstract <C /*>>> extends @NonNull Object*/> SpanContext extract(
+      C carrier, Getter<C> getter) throws SpanContextParseException;
+
+  /**
+   * Class that allows a {@code TextFormat} to read propagated fields from a carrier.
+   *
+   * <p>{@code Getter} is stateless and allows to be saved as a constant to avoid runtime
+   * allocations.
+   *
+   * @param <C> carrier of propagation fields, such as an http request
+   * @since 0.11
+   */
+  public abstract static class Getter<C> {
+
+    /**
+     * Returns the first value of the given propagation {@code key} or returns {@code null}.
+     *
+     * @param carrier carrier of propagation fields, such as an http request
+     * @param key the key of the field.
+     * @return the first value of the given propagation {@code key} or returns {@code null}.
+     * @since 0.11
+     */
+    @Nullable
+    public abstract String get(C carrier, String key);
+  }
+
+  /**
+   * Returns the no-op implementation of the {@code TextFormat}.
+   *
+   * @return the no-op implementation of the {@code TextFormat}.
+   */
+  static TextFormat getNoopTextFormat() {
+    return NOOP_TEXT_FORMAT;
+  }
+
+  private static final class NoopTextFormat extends TextFormat {
+
+    private NoopTextFormat() {}
+
+    @Override
+    public List<String> fields() {
+      return Collections.emptyList();
+    }
+
+    @Override
+    public <C /*>>> extends @NonNull Object*/> void inject(
+        SpanContext spanContext, C carrier, Setter<C> setter) {
+      Utils.checkNotNull(spanContext, "spanContext");
+      Utils.checkNotNull(carrier, "carrier");
+      Utils.checkNotNull(setter, "setter");
+    }
+
+    @Override
+    public <C /*>>> extends @NonNull Object*/> SpanContext extract(C carrier, Getter<C> getter) {
+      Utils.checkNotNull(carrier, "carrier");
+      Utils.checkNotNull(getter, "getter");
+      return SpanContext.INVALID;
+    }
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/samplers/AlwaysSampleSampler.java b/api/src/main/java/io/opencensus/trace/samplers/AlwaysSampleSampler.java
new file mode 100644
index 0000000..7b61e23
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/samplers/AlwaysSampleSampler.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.trace.samplers;
+
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/** Sampler that always makes a "yes" decision on {@link Span} sampling. */
+@Immutable
+final class AlwaysSampleSampler extends Sampler {
+
+  AlwaysSampleSampler() {}
+
+  // Returns always makes a "yes" decision on {@link Span} sampling.
+  @Override
+  public boolean shouldSample(
+      @Nullable SpanContext parentContext,
+      @Nullable Boolean hasRemoteParent,
+      TraceId traceId,
+      SpanId spanId,
+      String name,
+      List<Span> parentLinks) {
+    return true;
+  }
+
+  @Override
+  public String getDescription() {
+    return toString();
+  }
+
+  @Override
+  public String toString() {
+    return "AlwaysSampleSampler";
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/samplers/NeverSampleSampler.java b/api/src/main/java/io/opencensus/trace/samplers/NeverSampleSampler.java
new file mode 100644
index 0000000..c6de645
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/samplers/NeverSampleSampler.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.trace.samplers;
+
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/** Sampler that always makes a "no" decision on {@link Span} sampling. */
+@Immutable
+final class NeverSampleSampler extends Sampler {
+
+  NeverSampleSampler() {}
+
+  // Returns always makes a "no" decision on {@link Span} sampling.
+  @Override
+  public boolean shouldSample(
+      @Nullable SpanContext parentContext,
+      @Nullable Boolean hasRemoteParent,
+      TraceId traceId,
+      SpanId spanId,
+      String name,
+      List<Span> parentLinks) {
+    return false;
+  }
+
+  @Override
+  public String getDescription() {
+    return toString();
+  }
+
+  @Override
+  public String toString() {
+    return "NeverSampleSampler";
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/samplers/ProbabilitySampler.java b/api/src/main/java/io/opencensus/trace/samplers/ProbabilitySampler.java
new file mode 100644
index 0000000..b9c18e0
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/samplers/ProbabilitySampler.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.trace.samplers;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * We assume the lower 64 bits of the traceId's are randomly distributed around the whole (long)
+ * range. We convert an incoming probability into an upper bound on that value, such that we can
+ * just compare the absolute value of the id and the bound to see if we are within the desired
+ * probability range. Using the low bits of the traceId also ensures that systems that only use 64
+ * bit ID's will also work with this sampler.
+ */
+@AutoValue
+@Immutable
+abstract class ProbabilitySampler extends Sampler {
+
+  ProbabilitySampler() {}
+
+  abstract double getProbability();
+
+  abstract long getIdUpperBound();
+
+  /**
+   * Returns a new {@link ProbabilitySampler}. The probability of sampling a trace is equal to that
+   * of the specified probability.
+   *
+   * @param probability The desired probability of sampling. Must be within [0.0, 1.0].
+   * @return a new {@link ProbabilitySampler}.
+   * @throws IllegalArgumentException if {@code probability} is out of range
+   */
+  static ProbabilitySampler create(double probability) {
+    Utils.checkArgument(
+        probability >= 0.0 && probability <= 1.0, "probability must be in range [0.0, 1.0]");
+    long idUpperBound;
+    // Special case the limits, to avoid any possible issues with lack of precision across
+    // double/long boundaries. For probability == 0.0, we use Long.MIN_VALUE as this guarantees
+    // that we will never sample a trace, even in the case where the id == Long.MIN_VALUE, since
+    // Math.Abs(Long.MIN_VALUE) == Long.MIN_VALUE.
+    if (probability == 0.0) {
+      idUpperBound = Long.MIN_VALUE;
+    } else if (probability == 1.0) {
+      idUpperBound = Long.MAX_VALUE;
+    } else {
+      idUpperBound = (long) (probability * Long.MAX_VALUE);
+    }
+    return new AutoValue_ProbabilitySampler(probability, idUpperBound);
+  }
+
+  @Override
+  public final boolean shouldSample(
+      @Nullable SpanContext parentContext,
+      @Nullable Boolean hasRemoteParent,
+      TraceId traceId,
+      SpanId spanId,
+      String name,
+      @Nullable List<Span> parentLinks) {
+    // If the parent is sampled keep the sampling decision.
+    if (parentContext != null && parentContext.getTraceOptions().isSampled()) {
+      return true;
+    }
+    if (parentLinks != null) {
+      // If any parent link is sampled keep the sampling decision.
+      for (Span parentLink : parentLinks) {
+        if (parentLink.getContext().getTraceOptions().isSampled()) {
+          return true;
+        }
+      }
+    }
+    // Always sample if we are within probability range. This is true even for child spans (that
+    // may have had a different sampling decision made) to allow for different sampling policies,
+    // and dynamic increases to sampling probabilities for debugging purposes.
+    // Note use of '<' for comparison. This ensures that we never sample for probability == 0.0,
+    // while allowing for a (very) small chance of *not* sampling if the id == Long.MAX_VALUE.
+    // This is considered a reasonable tradeoff for the simplicity/performance requirements (this
+    // code is executed in-line for every Span creation).
+    return Math.abs(traceId.getLowerLong()) < getIdUpperBound();
+  }
+
+  @Override
+  public final String getDescription() {
+    return String.format("ProbabilitySampler{%.6f}", getProbability());
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/samplers/Samplers.java b/api/src/main/java/io/opencensus/trace/samplers/Samplers.java
new file mode 100644
index 0000000..c10610a
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/samplers/Samplers.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.trace.samplers;
+
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+
+/**
+ * Static class to access a set of pre-defined {@link Sampler Samplers}.
+ *
+ * @since 0.5
+ */
+public final class Samplers {
+  private static final Sampler ALWAYS_SAMPLE = new AlwaysSampleSampler();
+  private static final Sampler NEVER_SAMPLE = new NeverSampleSampler();
+
+  // No instance of this class.
+  private Samplers() {}
+
+  /**
+   * Returns a {@link Sampler} that always makes a "yes" decision on {@link Span} sampling.
+   *
+   * @return a {@code Sampler} that always makes a "yes" decision on {@code Span} sampling.
+   * @since 0.5
+   */
+  public static Sampler alwaysSample() {
+    return ALWAYS_SAMPLE;
+  }
+
+  /**
+   * Returns a {@link Sampler} that always makes a "no" decision on {@link Span} sampling.
+   *
+   * @return a {@code Sampler} that always makes a "no" decision on {@code Span} sampling.
+   * @since 0.5
+   */
+  public static Sampler neverSample() {
+    return NEVER_SAMPLE;
+  }
+
+  /**
+   * Returns a {@link Sampler} that makes a "yes" decision with a given probability.
+   *
+   * @param probability The desired probability of sampling. Must be within [0.0, 1.0].
+   * @return a {@code Sampler} that makes a "yes" decision with a given probability.
+   * @throws IllegalArgumentException if {@code probability} is out of range
+   * @since 0.5
+   */
+  public static Sampler probabilitySampler(double probability) {
+    return ProbabilitySampler.create(probability);
+  }
+}
diff --git a/api/src/main/java/io/opencensus/trace/unsafe/ContextUtils.java b/api/src/main/java/io/opencensus/trace/unsafe/ContextUtils.java
new file mode 100644
index 0000000..3f4b988
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/unsafe/ContextUtils.java
@@ -0,0 +1,45 @@
+/*
+ * 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.trace.unsafe;
+
+import io.grpc.Context;
+import io.opencensus.trace.Span;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Util methods/functionality to interact with the {@link io.grpc.Context}.
+ *
+ * <p>Users must interact with the current Context via the public APIs in {@link
+ * io.opencensus.trace.Tracer} and avoid usages of the {@link #CONTEXT_SPAN_KEY} directly.
+ *
+ * @since 0.5
+ */
+public final class ContextUtils {
+  // No instance of this class.
+  private ContextUtils() {}
+
+  /**
+   * The {@link io.grpc.Context.Key} used to interact with {@link io.grpc.Context}.
+   *
+   * @since 0.5
+   */
+  public static final Context.Key</*@Nullable*/ Span> CONTEXT_SPAN_KEY =
+      Context.key("opencensus-trace-span-key");
+}
diff --git a/api/src/test/java/io/opencensus/common/DurationTest.java b/api/src/test/java/io/opencensus/common/DurationTest.java
new file mode 100644
index 0000000..ea636ca
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/DurationTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Duration}. */
+@RunWith(JUnit4.class)
+public class DurationTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testDurationCreate() {
+    assertThat(Duration.create(24, 42).getSeconds()).isEqualTo(24);
+    assertThat(Duration.create(24, 42).getNanos()).isEqualTo(42);
+    assertThat(Duration.create(-24, -42).getSeconds()).isEqualTo(-24);
+    assertThat(Duration.create(-24, -42).getNanos()).isEqualTo(-42);
+    assertThat(Duration.create(315576000000L, 999999999).getSeconds()).isEqualTo(315576000000L);
+    assertThat(Duration.create(315576000000L, 999999999).getNanos()).isEqualTo(999999999);
+    assertThat(Duration.create(-315576000000L, -999999999).getSeconds()).isEqualTo(-315576000000L);
+    assertThat(Duration.create(-315576000000L, -999999999).getNanos()).isEqualTo(-999999999);
+  }
+
+  @Test
+  public void create_SecondsTooLow() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' is less than minimum (-315576000000): -315576000001");
+    Duration.create(-315576000001L, 0);
+  }
+
+  @Test
+  public void create_SecondsTooHigh() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' is greater than maximum (315576000000): 315576000001");
+    Duration.create(315576000001L, 0);
+  }
+
+  @Test
+  public void create_NanosTooLow() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'nanos' is less than minimum (-999999999): -1000000000");
+    Duration.create(0, -1000000000);
+  }
+
+  @Test
+  public void create_NanosTooHigh() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'nanos' is greater than maximum (999999999): 1000000000");
+    Duration.create(0, 1000000000);
+  }
+
+  @Test
+  public void create_NegativeSecondsPositiveNanos() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' and 'nanos' have inconsistent sign: seconds=-1, nanos=1");
+    Duration.create(-1, 1);
+  }
+
+  @Test
+  public void create_PositiveSecondsNegativeNanos() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' and 'nanos' have inconsistent sign: seconds=1, nanos=-1");
+    Duration.create(1, -1);
+  }
+
+  @Test
+  public void testDurationFromMillis() {
+    assertThat(Duration.fromMillis(0)).isEqualTo(Duration.create(0, 0));
+    assertThat(Duration.fromMillis(987)).isEqualTo(Duration.create(0, 987000000));
+    assertThat(Duration.fromMillis(3456)).isEqualTo(Duration.create(3, 456000000));
+  }
+
+  @Test
+  public void testDurationFromMillisNegative() {
+    assertThat(Duration.fromMillis(-1)).isEqualTo(Duration.create(0, -1000000));
+    assertThat(Duration.fromMillis(-999)).isEqualTo(Duration.create(0, -999000000));
+    assertThat(Duration.fromMillis(-1000)).isEqualTo(Duration.create(-1, 0));
+    assertThat(Duration.fromMillis(-3456)).isEqualTo(Duration.create(-3, -456000000));
+  }
+
+  @Test
+  public void fromMillis_TooLow() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' is less than minimum (-315576000000): -315576000001");
+    Duration.fromMillis(-315576000001000L);
+  }
+
+  @Test
+  public void fromMillis_TooHigh() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' is greater than maximum (315576000000): 315576000001");
+    Duration.fromMillis(315576000001000L);
+  }
+
+  @Test
+  public void duration_CompareLength() {
+    assertThat(Duration.create(0, 0).compareTo(Duration.create(0, 0))).isEqualTo(0);
+    assertThat(Duration.create(24, 42).compareTo(Duration.create(24, 42))).isEqualTo(0);
+    assertThat(Duration.create(-24, -42).compareTo(Duration.create(-24, -42))).isEqualTo(0);
+    assertThat(Duration.create(25, 42).compareTo(Duration.create(24, 42))).isEqualTo(1);
+    assertThat(Duration.create(24, 45).compareTo(Duration.create(24, 42))).isEqualTo(1);
+    assertThat(Duration.create(24, 42).compareTo(Duration.create(25, 42))).isEqualTo(-1);
+    assertThat(Duration.create(24, 42).compareTo(Duration.create(24, 45))).isEqualTo(-1);
+    assertThat(Duration.create(-24, -45).compareTo(Duration.create(-24, -42))).isEqualTo(-1);
+    assertThat(Duration.create(-24, -42).compareTo(Duration.create(-25, -42))).isEqualTo(1);
+    assertThat(Duration.create(24, 42).compareTo(Duration.create(-24, -42))).isEqualTo(1);
+  }
+
+  @Test
+  public void testDurationEqual() {
+    // Positive tests.
+    assertThat(Duration.create(0, 0)).isEqualTo(Duration.create(0, 0));
+    assertThat(Duration.create(24, 42)).isEqualTo(Duration.create(24, 42));
+    assertThat(Duration.create(-24, -42)).isEqualTo(Duration.create(-24, -42));
+    // Negative tests.
+    assertThat(Duration.create(25, 42)).isNotEqualTo(Duration.create(24, 42));
+    assertThat(Duration.create(24, 43)).isNotEqualTo(Duration.create(24, 42));
+    assertThat(Duration.create(-25, -42)).isNotEqualTo(Duration.create(-24, -42));
+    assertThat(Duration.create(-24, -43)).isNotEqualTo(Duration.create(-24, -42));
+  }
+
+  @Test
+  public void toMillis() {
+    assertThat(Duration.create(10, 0).toMillis()).isEqualTo(10000L);
+    assertThat(Duration.create(10, 1000).toMillis()).isEqualTo(10000L);
+    assertThat(Duration.create(0, (int) 1e6).toMillis()).isEqualTo(1L);
+    assertThat(Duration.create(0, 0).toMillis()).isEqualTo(0L);
+    assertThat(Duration.create(-10, 0).toMillis()).isEqualTo(-10000L);
+    assertThat(Duration.create(-10, -1000).toMillis()).isEqualTo(-10000L);
+    assertThat(Duration.create(0, -(int) 1e6).toMillis()).isEqualTo(-1L);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/common/FunctionsTest.java b/api/src/test/java/io/opencensus/common/FunctionsTest.java
new file mode 100644
index 0000000..55d58d4
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/FunctionsTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Functions}. */
+@RunWith(JUnit4.class)
+public class FunctionsTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testReturnNull() {
+    assertThat(Functions.returnNull().apply("ignored")).isNull();
+  }
+
+  @Test
+  public void testReturnConstant() {
+    assertThat(Functions.returnConstant(123).apply("ignored")).isEqualTo(123);
+  }
+
+  @Test
+  public void testReturnToString() {
+    assertThat(Functions.returnToString().apply("input")).isEqualTo("input");
+    assertThat(Functions.returnToString().apply(Boolean.FALSE)).isEqualTo("false");
+    assertThat(Functions.returnToString().apply(Double.valueOf(123.45))).isEqualTo("123.45");
+    assertThat(Functions.returnToString().apply(null)).isEqualTo(null);
+  }
+
+  @Test
+  public void testThrowIllegalArgumentException() {
+    Function<Object, Void> f = Functions.throwIllegalArgumentException();
+    thrown.expect(IllegalArgumentException.class);
+    f.apply("ignored");
+  }
+
+  @Test
+  public void testThrowAssertionError() {
+    Function<Object, Void> f = Functions.throwAssertionError();
+    thrown.handleAssertionErrors();
+    thrown.expect(AssertionError.class);
+    f.apply("ignored");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/common/ServerStatsEncodingTest.java b/api/src/test/java/io/opencensus/common/ServerStatsEncodingTest.java
new file mode 100644
index 0000000..6db14a7
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/ServerStatsEncodingTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018, 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ServerStatsEncoding}. */
+@RunWith(JUnit4.class)
+public class ServerStatsEncodingTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void encodeDecodeTest() throws ServerStatsDeserializationException {
+    ServerStats serverStatsToBeEncoded = null;
+    ServerStats serverStatsDecoded = null;
+    byte[] serialized = null;
+
+    serverStatsToBeEncoded = ServerStats.create(31, 22, (byte) 1);
+    serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+    serverStatsDecoded = ServerStatsEncoding.parseBytes(serialized);
+    assertThat(serverStatsDecoded).isEqualTo(serverStatsToBeEncoded);
+
+    serverStatsToBeEncoded = ServerStats.create(0, 22, (byte) 0);
+    serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+    serverStatsDecoded = ServerStatsEncoding.parseBytes(serialized);
+    assertThat(serverStatsDecoded).isEqualTo(serverStatsToBeEncoded);
+
+    serverStatsToBeEncoded = ServerStats.create(450, 0, (byte) 0);
+    serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+    serverStatsDecoded = ServerStatsEncoding.parseBytes(serialized);
+    assertThat(serverStatsDecoded).isEqualTo(serverStatsToBeEncoded);
+  }
+
+  @Test
+  public void skipUnknownFieldTest() throws ServerStatsDeserializationException {
+    ServerStats serverStatsToBeEncoded = null;
+    ServerStats serverStatsDecoded = null;
+    byte[] serialized = null;
+
+    serverStatsToBeEncoded = ServerStats.create(31, 22, (byte) 1);
+    serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+
+    // Add new field at the end.
+    byte[] serializedExpanded = new byte[serialized.length + 9];
+    System.arraycopy(serialized, 0, serializedExpanded, 0, serialized.length);
+    final ByteBuffer bb = ByteBuffer.wrap(serializedExpanded);
+    bb.order(ByteOrder.LITTLE_ENDIAN);
+    bb.position(serialized.length);
+    bb.put((byte) 255);
+    bb.putLong(0L);
+    byte[] newSerialized = bb.array();
+
+    serverStatsDecoded = ServerStatsEncoding.parseBytes(newSerialized);
+    assertThat(serverStatsDecoded).isEqualTo(serverStatsToBeEncoded);
+  }
+
+  @Test
+  public void negativeLbLatencyValueTest() throws ServerStatsDeserializationException {
+    ServerStats serverStatsToBeEncoded = null;
+    ServerStats serverStatsDecoded = null;
+    byte[] serialized = null;
+
+    serverStatsToBeEncoded = ServerStats.create(31, 22, (byte) 1);
+    serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+
+    // update serialized byte[] with negative value for lbLatency.
+    final ByteBuffer bb = ByteBuffer.wrap(serialized);
+    bb.order(ByteOrder.LITTLE_ENDIAN);
+    bb.position(2);
+    bb.putLong(-100L);
+
+    byte[] newSerialized = bb.array();
+    thrown.expect(ServerStatsDeserializationException.class);
+    thrown.expectMessage("Serialized ServiceStats contains invalid values");
+    ServerStatsEncoding.parseBytes(newSerialized);
+  }
+
+  @Test
+  public void negativeServerLatencyValueTest() throws ServerStatsDeserializationException {
+    ServerStats serverStatsToBeEncoded = null;
+    ServerStats serverStatsDecoded = null;
+    byte[] serialized = null;
+
+    serverStatsToBeEncoded = ServerStats.create(31, 22, (byte) 1);
+    serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+
+    // update serialized byte[] with negative value for serviceLatency.
+    final ByteBuffer bb = ByteBuffer.wrap(serialized);
+    bb.order(ByteOrder.LITTLE_ENDIAN);
+    bb.position(11);
+    bb.putLong(-101L);
+
+    byte[] newSerialized = bb.array();
+    thrown.expect(ServerStatsDeserializationException.class);
+    thrown.expectMessage("Serialized ServiceStats contains invalid values");
+    ServerStatsEncoding.parseBytes(newSerialized);
+  }
+
+  @Test
+  public void emptySerializedBuffer() throws ServerStatsDeserializationException {
+    final ByteBuffer bb = ByteBuffer.allocate(0);
+    bb.order(ByteOrder.LITTLE_ENDIAN);
+
+    byte[] newSerialized = bb.array();
+    thrown.expect(ServerStatsDeserializationException.class);
+    thrown.expectMessage("Serialized ServerStats buffer is empty");
+    ServerStatsEncoding.parseBytes(newSerialized);
+  }
+
+  @Test
+  public void invalidNegativeVersion() throws ServerStatsDeserializationException {
+    final ByteBuffer bb = ByteBuffer.allocate(10);
+    bb.order(ByteOrder.LITTLE_ENDIAN);
+    bb.put((byte) -1);
+    byte[] newSerialized = bb.array();
+    thrown.expect(ServerStatsDeserializationException.class);
+    thrown.expectMessage("Invalid ServerStats version: -1");
+    ServerStatsEncoding.parseBytes(newSerialized);
+  }
+
+  @Test
+  public void invalidCompatibleVersion() throws ServerStatsDeserializationException {
+    final ByteBuffer bb = ByteBuffer.allocate(10);
+    bb.order(ByteOrder.LITTLE_ENDIAN);
+    bb.put((byte) (ServerStatsEncoding.CURRENT_VERSION + 1));
+    byte[] newSerialized = bb.array();
+    thrown.expect(ServerStatsDeserializationException.class);
+    thrown.expectMessage(
+        "Invalid ServerStats version: " + (ServerStatsEncoding.CURRENT_VERSION + 1));
+    ServerStatsEncoding.parseBytes(newSerialized);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/common/ServerStatsFieldEnumsTest.java b/api/src/test/java/io/opencensus/common/ServerStatsFieldEnumsTest.java
new file mode 100644
index 0000000..ed786f6
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/ServerStatsFieldEnumsTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2018, 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.ServerStatsFieldEnums.Id;
+import io.opencensus.common.ServerStatsFieldEnums.Size;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ServerStatsFieldEnums}. */
+@RunWith(JUnit4.class)
+public class ServerStatsFieldEnumsTest {
+
+  @Test
+  public void enumIdValueOfTest() {
+    assertThat(Id.valueOf(0)).isEqualTo(Id.SERVER_STATS_LB_LATENCY_ID);
+    assertThat(Id.valueOf(1)).isEqualTo(Id.SERVER_STATS_SERVICE_LATENCY_ID);
+    assertThat(Id.valueOf(2)).isEqualTo(Id.SERVER_STATS_TRACE_OPTION_ID);
+  }
+
+  @Test
+  public void enumIdInvalidValueOfTest() {
+    assertThat(Id.valueOf(-1)).isNull();
+    assertThat(Id.valueOf(Id.values().length)).isNull();
+    assertThat(Id.valueOf(Id.values().length + 1)).isNull();
+  }
+
+  @Test
+  public void enumSizeValueTest() {
+    assertThat(Size.SERVER_STATS_LB_LATENCY_SIZE.value()).isEqualTo(8);
+    assertThat(Size.SERVER_STATS_SERVICE_LATENCY_SIZE.value()).isEqualTo(8);
+    assertThat(Size.SERVER_STATS_TRACE_OPTION_SIZE.value()).isEqualTo(1);
+  }
+
+  @Test
+  public void totalSizeTest() {
+    assertThat(ServerStatsFieldEnums.getTotalSize()).isEqualTo(20);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/common/ServerStatsTest.java b/api/src/test/java/io/opencensus/common/ServerStatsTest.java
new file mode 100644
index 0000000..620bbb4
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/ServerStatsTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018, 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ServerStats}. */
+@RunWith(JUnit4.class)
+public class ServerStatsTest {
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void serverStatsCreate() {
+    ServerStats serverStats = null;
+
+    serverStats = ServerStats.create(31, 22, (byte) 0);
+    assertThat(serverStats.getLbLatencyNs()).isEqualTo(31);
+    assertThat(serverStats.getServiceLatencyNs()).isEqualTo(22);
+    assertThat(serverStats.getTraceOption()).isEqualTo((byte) 0);
+
+    serverStats = ServerStats.create(1000011L, 900022L, (byte) 1);
+    assertThat(serverStats.getLbLatencyNs()).isEqualTo(1000011L);
+    assertThat(serverStats.getServiceLatencyNs()).isEqualTo(900022L);
+    assertThat(serverStats.getTraceOption()).isEqualTo((byte) 1);
+
+    serverStats = ServerStats.create(0, 22, (byte) 0);
+    assertThat(serverStats.getLbLatencyNs()).isEqualTo(0);
+    assertThat(serverStats.getServiceLatencyNs()).isEqualTo(22);
+    assertThat(serverStats.getTraceOption()).isEqualTo((byte) 0);
+
+    serverStats = ServerStats.create(1010, 0, (byte) 0);
+    assertThat(serverStats.getLbLatencyNs()).isEqualTo(1010);
+    assertThat(serverStats.getServiceLatencyNs()).isEqualTo(0);
+    assertThat(serverStats.getTraceOption()).isEqualTo((byte) 0);
+  }
+
+  @Test
+  public void create_LbLatencyNegative() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'getLbLatencyNs' is less than zero");
+    ServerStats.create(-1L, 100, (byte) 0);
+  }
+
+  @Test
+  public void create_ServerLatencyNegative() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'getServiceLatencyNs' is less than zero");
+    ServerStats.create(100L, -1L, (byte) 0);
+  }
+
+  @Test
+  public void create_LbLatencyAndServerLatencyNegative() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'getLbLatencyNs' is less than zero");
+    ServerStats.create(-100L, -1L, (byte) 0);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/common/TimeUtilsTest.java b/api/src/test/java/io/opencensus/common/TimeUtilsTest.java
new file mode 100644
index 0000000..d622856
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/TimeUtilsTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018, 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TimeUtils}. */
+@RunWith(JUnit4.class)
+public final class TimeUtilsTest {
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void compareLongs() {
+    assertThat(TimeUtils.compareLongs(-1L, 1L)).isLessThan(0);
+    assertThat(TimeUtils.compareLongs(10L, 10L)).isEqualTo(0);
+    assertThat(TimeUtils.compareLongs(1L, 0L)).isGreaterThan(0);
+  }
+
+  @Test
+  public void checkedAdd_TooLow() {
+    thrown.expect(ArithmeticException.class);
+    thrown.expectMessage("Long sum overflow: x=-9223372036854775807, y=-2");
+    TimeUtils.checkedAdd(Long.MIN_VALUE + 1, -2);
+  }
+
+  @Test
+  public void checkedAdd_TooHigh() {
+    thrown.expect(ArithmeticException.class);
+    thrown.expectMessage("Long sum overflow: x=9223372036854775806, y=2");
+    TimeUtils.checkedAdd(Long.MAX_VALUE - 1, 2);
+  }
+
+  @Test
+  public void checkedAdd_Valid() {
+    assertThat(TimeUtils.checkedAdd(1, 2)).isEqualTo(3);
+    assertThat(TimeUtils.checkedAdd(Integer.MAX_VALUE, Integer.MAX_VALUE))
+        .isEqualTo(2L * Integer.MAX_VALUE);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/common/TimestampTest.java b/api/src/test/java/io/opencensus/common/TimestampTest.java
new file mode 100644
index 0000000..b193b3c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/TimestampTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Timestamp}. */
+@RunWith(JUnit4.class)
+public class TimestampTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void timestampCreate() {
+    assertThat(Timestamp.create(24, 42).getSeconds()).isEqualTo(24);
+    assertThat(Timestamp.create(24, 42).getNanos()).isEqualTo(42);
+    assertThat(Timestamp.create(-24, 42).getSeconds()).isEqualTo(-24);
+    assertThat(Timestamp.create(-24, 42).getNanos()).isEqualTo(42);
+    assertThat(Timestamp.create(315576000000L, 999999999).getSeconds()).isEqualTo(315576000000L);
+    assertThat(Timestamp.create(315576000000L, 999999999).getNanos()).isEqualTo(999999999);
+    assertThat(Timestamp.create(-315576000000L, 999999999).getSeconds()).isEqualTo(-315576000000L);
+    assertThat(Timestamp.create(-315576000000L, 999999999).getNanos()).isEqualTo(999999999);
+  }
+
+  @Test
+  public void create_SecondsTooLow() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' is less than minimum (-315576000000): -315576000001");
+    Timestamp.create(-315576000001L, 0);
+  }
+
+  @Test
+  public void create_SecondsTooHigh() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' is greater than maximum (315576000000): 315576000001");
+    Timestamp.create(315576000001L, 0);
+  }
+
+  @Test
+  public void create_NanosTooLow_PositiveTime() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'nanos' is less than zero: -1");
+    Timestamp.create(1, -1);
+  }
+
+  @Test
+  public void create_NanosTooHigh_PositiveTime() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'nanos' is greater than maximum (999999999): 1000000000");
+    Timestamp.create(1, 1000000000);
+  }
+
+  @Test
+  public void create_NanosTooLow_NegativeTime() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'nanos' is less than zero: -1");
+    Timestamp.create(-1, -1);
+  }
+
+  @Test
+  public void create_NanosTooHigh_NegativeTime() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'nanos' is greater than maximum (999999999): 1000000000");
+    Timestamp.create(-1, 1000000000);
+  }
+
+  @Test
+  public void timestampFromMillis() {
+    assertThat(Timestamp.fromMillis(0)).isEqualTo(Timestamp.create(0, 0));
+    assertThat(Timestamp.fromMillis(987)).isEqualTo(Timestamp.create(0, 987000000));
+    assertThat(Timestamp.fromMillis(3456)).isEqualTo(Timestamp.create(3, 456000000));
+  }
+
+  @Test
+  public void timestampFromMillis_Negative() {
+    assertThat(Timestamp.fromMillis(-1)).isEqualTo(Timestamp.create(-1, 999000000));
+    assertThat(Timestamp.fromMillis(-999)).isEqualTo(Timestamp.create(-1, 1000000));
+    assertThat(Timestamp.fromMillis(-3456)).isEqualTo(Timestamp.create(-4, 544000000));
+  }
+
+  @Test
+  public void fromMillis_TooLow() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' is less than minimum (-315576000000): -315576000001");
+    Timestamp.fromMillis(-315576000001000L);
+  }
+
+  @Test
+  public void fromMillis_TooHigh() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("'seconds' is greater than maximum (315576000000): 315576000001");
+    Timestamp.fromMillis(315576000001000L);
+  }
+
+  @Test
+  public void timestampAddNanos() {
+    Timestamp timestamp = Timestamp.create(1234, 223);
+    assertThat(timestamp.addNanos(0)).isEqualTo(timestamp);
+    assertThat(timestamp.addNanos(999999777)).isEqualTo(Timestamp.create(1235, 0));
+    assertThat(timestamp.addNanos(1300200500)).isEqualTo(Timestamp.create(1235, 300200723));
+    assertThat(timestamp.addNanos(1999999777)).isEqualTo(Timestamp.create(1236, 0));
+    assertThat(timestamp.addNanos(9876543789L)).isEqualTo(Timestamp.create(1243, 876544012));
+    assertThat(timestamp.addNanos(Long.MAX_VALUE))
+        .isEqualTo(Timestamp.create(1234L + 9223372036L, 223 + 854775807));
+  }
+
+  @Test
+  public void timestampAddNanos_Negative() {
+    Timestamp timestamp = Timestamp.create(1234, 223);
+    assertThat(timestamp.addNanos(-223)).isEqualTo(Timestamp.create(1234, 0));
+    assertThat(timestamp.addNanos(-1000000223)).isEqualTo(Timestamp.create(1233, 0));
+    assertThat(timestamp.addNanos(-1300200500)).isEqualTo(Timestamp.create(1232, 699799723));
+    assertThat(timestamp.addNanos(-4123456213L)).isEqualTo(Timestamp.create(1229, 876544010));
+    assertThat(timestamp.addNanos(Long.MIN_VALUE))
+        .isEqualTo(Timestamp.create(1234L - 9223372036L - 1, 223 + 145224192));
+  }
+
+  @Test
+  public void timestampAddDuration() {
+    Timestamp timestamp = Timestamp.create(1234, 223);
+    assertThat(timestamp.addDuration(Duration.create(1, 0))).isEqualTo(Timestamp.create(1235, 223));
+    assertThat(timestamp.addDuration(Duration.create(0, 1))).isEqualTo(Timestamp.create(1234, 224));
+    assertThat(timestamp.addDuration(Duration.create(1, 1))).isEqualTo(Timestamp.create(1235, 224));
+    assertThat(timestamp.addDuration(Duration.create(1, 999999900)))
+        .isEqualTo(Timestamp.create(1236, 123));
+  }
+
+  @Test
+  public void timestampAddDuration_Negative() {
+    Timestamp timestamp = Timestamp.create(1234, 223);
+    assertThat(timestamp.addDuration(Duration.create(-1234, -223)))
+        .isEqualTo(Timestamp.create(0, 0));
+    assertThat(timestamp.addDuration(Duration.create(-1, 0)))
+        .isEqualTo(Timestamp.create(1233, 223));
+    assertThat(timestamp.addDuration(Duration.create(-1, -1)))
+        .isEqualTo(Timestamp.create(1233, 222));
+    assertThat(timestamp.addDuration(Duration.create(-1, -323)))
+        .isEqualTo(Timestamp.create(1232, 999999900));
+    assertThat(timestamp.addDuration(Duration.create(-33, -999999999)))
+        .isEqualTo(Timestamp.create(1200, 224));
+  }
+
+  @Test
+  public void timestampSubtractTimestamp() {
+    Timestamp timestamp = Timestamp.create(1234, 223);
+    assertThat(timestamp.subtractTimestamp(Timestamp.create(0, 0)))
+        .isEqualTo(Duration.create(1234, 223));
+    assertThat(timestamp.subtractTimestamp(Timestamp.create(1233, 223)))
+        .isEqualTo(Duration.create(1, 0));
+    assertThat(timestamp.subtractTimestamp(Timestamp.create(1233, 222)))
+        .isEqualTo(Duration.create(1, 1));
+    assertThat(timestamp.subtractTimestamp(Timestamp.create(1232, 999999900)))
+        .isEqualTo(Duration.create(1, 323));
+    assertThat(timestamp.subtractTimestamp(Timestamp.create(1200, 224)))
+        .isEqualTo(Duration.create(33, 999999999));
+  }
+
+  @Test
+  public void timestampSubtractTimestamp_NegativeResult() {
+    Timestamp timestamp = Timestamp.create(1234, 223);
+    assertThat(timestamp.subtractTimestamp(Timestamp.create(1235, 223)))
+        .isEqualTo(Duration.create(-1, 0));
+    assertThat(timestamp.subtractTimestamp(Timestamp.create(1234, 224)))
+        .isEqualTo(Duration.create(0, -1));
+    assertThat(timestamp.subtractTimestamp(Timestamp.create(1235, 224)))
+        .isEqualTo(Duration.create(-1, -1));
+    assertThat(timestamp.subtractTimestamp(Timestamp.create(1236, 123)))
+        .isEqualTo(Duration.create(-1, -999999900));
+  }
+
+  @Test
+  public void timestamp_CompareTo() {
+    assertThat(Timestamp.create(0, 0).compareTo(Timestamp.create(0, 0))).isEqualTo(0);
+    assertThat(Timestamp.create(24, 42).compareTo(Timestamp.create(24, 42))).isEqualTo(0);
+    assertThat(Timestamp.create(-24, 42).compareTo(Timestamp.create(-24, 42))).isEqualTo(0);
+    assertThat(Timestamp.create(25, 42).compareTo(Timestamp.create(24, 42))).isEqualTo(1);
+    assertThat(Timestamp.create(24, 45).compareTo(Timestamp.create(24, 42))).isEqualTo(1);
+    assertThat(Timestamp.create(24, 42).compareTo(Timestamp.create(25, 42))).isEqualTo(-1);
+    assertThat(Timestamp.create(24, 42).compareTo(Timestamp.create(24, 45))).isEqualTo(-1);
+    assertThat(Timestamp.create(-25, 42).compareTo(Timestamp.create(-24, 42))).isEqualTo(-1);
+    assertThat(Timestamp.create(-24, 45).compareTo(Timestamp.create(-24, 42))).isEqualTo(1);
+    assertThat(Timestamp.create(-24, 42).compareTo(Timestamp.create(-25, 42))).isEqualTo(1);
+    assertThat(Timestamp.create(-24, 42).compareTo(Timestamp.create(-24, 45))).isEqualTo(-1);
+  }
+
+  @Test
+  public void timestamp_Equal() {
+    // Positive tests.
+    assertThat(Timestamp.create(0, 0)).isEqualTo(Timestamp.create(0, 0));
+    assertThat(Timestamp.create(24, 42)).isEqualTo(Timestamp.create(24, 42));
+    assertThat(Timestamp.create(-24, 42)).isEqualTo(Timestamp.create(-24, 42));
+    // Negative tests.
+    assertThat(Timestamp.create(25, 42)).isNotEqualTo(Timestamp.create(24, 42));
+    assertThat(Timestamp.create(24, 43)).isNotEqualTo(Timestamp.create(24, 42));
+    assertThat(Timestamp.create(-25, 42)).isNotEqualTo(Timestamp.create(-24, 42));
+    assertThat(Timestamp.create(-24, 43)).isNotEqualTo(Timestamp.create(-24, 42));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/internal/ProviderTest.java b/api/src/test/java/io/opencensus/internal/ProviderTest.java
new file mode 100644
index 0000000..1f4c33f
--- /dev/null
+++ b/api/src/test/java/io/opencensus/internal/ProviderTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2016-17, 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.ServiceConfigurationError;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Provider}. */
+@RunWith(JUnit4.class)
+public class ProviderTest {
+  static class GoodClass {
+    public GoodClass() {}
+  }
+
+  static class PrivateConstructorClass {
+    private PrivateConstructorClass() {}
+  }
+
+  static class NoDefaultConstructorClass {
+    public NoDefaultConstructorClass(int arg) {}
+  }
+
+  private static class PrivateClass {}
+
+  static interface MyInterface {}
+
+  static class MyInterfaceImpl implements MyInterface {
+    public MyInterfaceImpl() {}
+  }
+
+  @Test(expected = ServiceConfigurationError.class)
+  public void createInstance_ThrowsErrorWhenClassIsPrivate() throws ClassNotFoundException {
+    Provider.createInstance(
+        Class.forName(
+            "io.opencensus.internal.ProviderTest$PrivateClass",
+            true,
+            ProviderTest.class.getClassLoader()),
+        PrivateClass.class);
+  }
+
+  @Test(expected = ServiceConfigurationError.class)
+  public void createInstance_ThrowsErrorWhenClassHasPrivateConstructor()
+      throws ClassNotFoundException {
+    Provider.createInstance(
+        Class.forName(
+            "io.opencensus.internal.ProviderTest$PrivateConstructorClass",
+            true,
+            ProviderTest.class.getClassLoader()),
+        PrivateConstructorClass.class);
+  }
+
+  @Test(expected = ServiceConfigurationError.class)
+  public void createInstance_ThrowsErrorWhenClassDoesNotHaveDefaultConstructor()
+      throws ClassNotFoundException {
+    Provider.createInstance(
+        Class.forName(
+            "io.opencensus.internal.ProviderTest$NoDefaultConstructorClass",
+            true,
+            ProviderTest.class.getClassLoader()),
+        NoDefaultConstructorClass.class);
+  }
+
+  @Test(expected = ServiceConfigurationError.class)
+  public void createInstance_ThrowsErrorWhenClassIsNotASubclass() throws ClassNotFoundException {
+    Provider.createInstance(
+        Class.forName(
+            "io.opencensus.internal.ProviderTest$GoodClass",
+            true,
+            ProviderTest.class.getClassLoader()),
+        MyInterface.class);
+  }
+
+  @Test
+  public void createInstance_GoodClass() throws ClassNotFoundException {
+    assertThat(
+            Provider.createInstance(
+                Class.forName(
+                    "io.opencensus.internal.ProviderTest$GoodClass",
+                    true,
+                    ProviderTest.class.getClassLoader()),
+                GoodClass.class))
+        .isNotNull();
+  }
+
+  @Test
+  public void createInstance_GoodSubclass() throws ClassNotFoundException {
+    assertThat(
+            Provider.createInstance(
+                Class.forName(
+                    "io.opencensus.internal.ProviderTest$MyInterfaceImpl",
+                    true,
+                    ProviderTest.class.getClassLoader()),
+                MyInterface.class))
+        .isNotNull();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/internal/StringUtilsTest.java b/api/src/test/java/io/opencensus/internal/StringUtilsTest.java
new file mode 100644
index 0000000..5e86694
--- /dev/null
+++ b/api/src/test/java/io/opencensus/internal/StringUtilsTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016-17, 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.internal;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link StringUtils}. */
+@RunWith(JUnit4.class)
+public final class StringUtilsTest {
+
+  @Test
+  public void isPrintableString() {
+    assertTrue(StringUtils.isPrintableString("abcd"));
+    assertFalse(StringUtils.isPrintableString("\2ab\3cd"));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/internal/UtilsTest.java b/api/src/test/java/io/opencensus/internal/UtilsTest.java
new file mode 100644
index 0000000..608a8fe
--- /dev/null
+++ b/api/src/test/java/io/opencensus/internal/UtilsTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018, 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.internal;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Date;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Utils}. */
+@RunWith(JUnit4.class)
+public final class UtilsTest {
+  private static final String TEST_MESSAGE = "test message";
+  private static final String TEST_MESSAGE_TEMPLATE = "I ate %s eggs.";
+  private static final int TEST_MESSAGE_VALUE = 2;
+  private static final String FORMATED_SIMPLE_TEST_MESSAGE = "I ate 2 eggs.";
+  private static final String FORMATED_COMPLEX_TEST_MESSAGE = "I ate 2 eggs. [2]";
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void checkArgument() {
+    Utils.checkArgument(true, TEST_MESSAGE);
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage(TEST_MESSAGE);
+    Utils.checkArgument(false, TEST_MESSAGE);
+  }
+
+  @Test
+  public void checkArgument_NullErrorMessage() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("null");
+    Utils.checkArgument(false, null);
+  }
+
+  @Test
+  public void checkArgument_WithSimpleFormat() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage(FORMATED_SIMPLE_TEST_MESSAGE);
+    Utils.checkArgument(false, TEST_MESSAGE_TEMPLATE, TEST_MESSAGE_VALUE);
+  }
+
+  @Test
+  public void checkArgument_WithComplexFormat() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage(FORMATED_COMPLEX_TEST_MESSAGE);
+    Utils.checkArgument(false, TEST_MESSAGE_TEMPLATE, TEST_MESSAGE_VALUE, TEST_MESSAGE_VALUE);
+  }
+
+  @Test
+  public void checkState() {
+    Utils.checkNotNull(true, TEST_MESSAGE);
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage(TEST_MESSAGE);
+    Utils.checkState(false, TEST_MESSAGE);
+  }
+
+  @Test
+  public void checkState_NullErrorMessage() {
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("null");
+    Utils.checkState(false, null);
+  }
+
+  @Test
+  public void checkNotNull() {
+    Utils.checkNotNull(new Object(), TEST_MESSAGE);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(TEST_MESSAGE);
+    Utils.checkNotNull(null, TEST_MESSAGE);
+  }
+
+  @Test
+  public void checkNotNull_NullErrorMessage() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("null");
+    Utils.checkNotNull(null, null);
+  }
+
+  @Test
+  public void checkIndex_Valid() {
+    Utils.checkIndex(1, 2);
+  }
+
+  @Test
+  public void checkIndex_NegativeSize() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Negative size: -1");
+    Utils.checkIndex(0, -1);
+  }
+
+  @Test
+  public void checkIndex_NegativeIndex() {
+    thrown.expect(IndexOutOfBoundsException.class);
+    thrown.expectMessage("Index out of bounds: size=10, index=-2");
+    Utils.checkIndex(-2, 10);
+  }
+
+  @Test
+  public void checkIndex_IndexEqualToSize() {
+    thrown.expect(IndexOutOfBoundsException.class);
+    thrown.expectMessage("Index out of bounds: size=5, index=5");
+    Utils.checkIndex(5, 5);
+  }
+
+  @Test
+  public void checkIndex_IndexGreaterThanSize() {
+    thrown.expect(IndexOutOfBoundsException.class);
+    thrown.expectMessage("Index out of bounds: size=10, index=11");
+    Utils.checkIndex(11, 10);
+  }
+
+  @Test
+  public void equalsObjects_Equal() {
+    assertTrue(Utils.equalsObjects(null, null));
+    assertTrue(Utils.equalsObjects(new Date(1L), new Date(1L)));
+  }
+
+  @Test
+  public void equalsObjects_Unequal() {
+    assertFalse(Utils.equalsObjects(null, new Object()));
+    assertFalse(Utils.equalsObjects(new Object(), null));
+    assertFalse(Utils.equalsObjects(new Object(), new Object()));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/DerivedDoubleGaugeTest.java b/api/src/test/java/io/opencensus/metrics/DerivedDoubleGaugeTest.java
new file mode 100644
index 0000000..dbae3c4
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/DerivedDoubleGaugeTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ToDoubleFunction;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DerivedDoubleGauge}. */
+// TODO(mayurkale): Add more tests, once DerivedDoubleGauge plugs-in into the registry.
+@RunWith(JUnit4.class)
+public class DerivedDoubleGaugeTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String NAME = "name";
+  private static final String DESCRIPTION = "description";
+  private static final String UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+  private static final List<LabelValue> EMPTY_LABEL_VALUES = new ArrayList<LabelValue>();
+
+  private final DerivedDoubleGauge derivedDoubleGauge =
+      DerivedDoubleGauge.newNoopDerivedDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+  private static final ToDoubleFunction<Object> doubleFunction =
+      new ToDoubleFunction<Object>() {
+        @Override
+        public double applyAsDouble(Object value) {
+          return 5.0;
+        }
+      };
+
+  @Test
+  public void noopCreateTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    derivedDoubleGauge.createTimeSeries(null, null, doubleFunction);
+  }
+
+  @Test
+  public void noopCreateTimeSeries_WithNullElement() {
+    List<LabelValue> labelValues = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValue element should not be null.");
+    derivedDoubleGauge.createTimeSeries(labelValues, null, doubleFunction);
+  }
+
+  @Test
+  public void noopCreateTimeSeries_WithInvalidLabelSize() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Incorrect number of labels.");
+    derivedDoubleGauge.createTimeSeries(EMPTY_LABEL_VALUES, null, doubleFunction);
+  }
+
+  @Test
+  public void createTimeSeries_WithNullFunction() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("function");
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, null);
+  }
+
+  @Test
+  public void noopRemoveTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    derivedDoubleGauge.removeTimeSeries(null);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/DerivedLongGaugeTest.java b/api/src/test/java/io/opencensus/metrics/DerivedLongGaugeTest.java
new file mode 100644
index 0000000..6a46288
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/DerivedLongGaugeTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ToLongFunction;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DerivedLongGauge}. */
+// TODO(mayurkale): Add more tests, once DerivedLongGauge plugs-in into the registry.
+@RunWith(JUnit4.class)
+public class DerivedLongGaugeTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String NAME = "name";
+  private static final String DESCRIPTION = "description";
+  private static final String UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+  private static final List<LabelValue> EMPTY_LABEL_VALUES = new ArrayList<LabelValue>();
+
+  private final DerivedLongGauge derivedLongGauge =
+      DerivedLongGauge.newNoopDerivedLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+  private static final ToLongFunction<Object> longFunction =
+      new ToLongFunction<Object>() {
+        @Override
+        public long applyAsLong(Object value) {
+          return 5;
+        }
+      };
+
+  @Test
+  public void noopCreateTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    derivedLongGauge.createTimeSeries(null, null, longFunction);
+  }
+
+  @Test
+  public void noopCreateTimeSeries_WithNullElement() {
+    List<LabelValue> labelValues = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValue element should not be null.");
+    derivedLongGauge.createTimeSeries(labelValues, null, longFunction);
+  }
+
+  @Test
+  public void noopCreateTimeSeries_WithInvalidLabelSize() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Incorrect number of labels.");
+    derivedLongGauge.createTimeSeries(EMPTY_LABEL_VALUES, null, longFunction);
+  }
+
+  @Test
+  public void createTimeSeries_WithNullFunction() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("function");
+    derivedLongGauge.createTimeSeries(LABEL_VALUES, null, null);
+  }
+
+  @Test
+  public void noopRemoveTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    derivedLongGauge.removeTimeSeries(null);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/DoubleGaugeTest.java b/api/src/test/java/io/opencensus/metrics/DoubleGaugeTest.java
new file mode 100644
index 0000000..b0cdea7
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/DoubleGaugeTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DoubleGauge}. */
+@RunWith(JUnit4.class)
+public class DoubleGaugeTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String NAME = "name";
+  private static final String DESCRIPTION = "description";
+  private static final String UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+  private static final List<LabelKey> EMPTY_LABEL_KEYS = new ArrayList<LabelKey>();
+  private static final List<LabelValue> EMPTY_LABEL_VALUES = new ArrayList<LabelValue>();
+
+  // TODO(mayurkale): Add more tests, once DoubleGauge plugs-in into the registry.
+
+  @Test
+  public void noopGetOrCreateTimeSeries_WithNullLabelValues() {
+    DoubleGauge doubleGauge =
+        DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, EMPTY_LABEL_KEYS);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    doubleGauge.getOrCreateTimeSeries(null);
+  }
+
+  @Test
+  public void noopGetOrCreateTimeSeries_WithNullElement() {
+    List<LabelValue> labelValues = Collections.singletonList(null);
+    DoubleGauge doubleGauge = DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValue element should not be null.");
+    doubleGauge.getOrCreateTimeSeries(labelValues);
+  }
+
+  @Test
+  public void noopGetOrCreateTimeSeries_WithInvalidLabelSize() {
+    DoubleGauge doubleGauge = DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Incorrect number of labels.");
+    doubleGauge.getOrCreateTimeSeries(EMPTY_LABEL_VALUES);
+  }
+
+  @Test
+  public void noopRemoveTimeSeries_WithNullLabelValues() {
+    DoubleGauge doubleGauge = DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    doubleGauge.removeTimeSeries(null);
+  }
+
+  @Test
+  public void noopSameAs() {
+    DoubleGauge doubleGauge = DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    assertThat(doubleGauge.getDefaultTimeSeries()).isSameAs(doubleGauge.getDefaultTimeSeries());
+    assertThat(doubleGauge.getDefaultTimeSeries())
+        .isSameAs(doubleGauge.getOrCreateTimeSeries(LABEL_VALUES));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/LabelKeyTest.java b/api/src/test/java/io/opencensus/metrics/LabelKeyTest.java
new file mode 100644
index 0000000..83f2b59
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/LabelKeyTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link LabelKey}. */
+@RunWith(JUnit4.class)
+public class LabelKeyTest {
+
+  private static final LabelKey KEY = LabelKey.create("key", "description");
+
+  @Test
+  public void testGetKey() {
+    assertThat(KEY.getKey()).isEqualTo("key");
+  }
+
+  @Test
+  public void testGetDescription() {
+    assertThat(KEY.getDescription()).isEqualTo("description");
+  }
+
+  @Test
+  public void create_NoLengthConstraint() {
+    // We have a length constraint of 256-characters for TagKey. That constraint doesn't apply to
+    // LabelKey.
+    char[] chars = new char[300];
+    Arrays.fill(chars, 'k');
+    String key = new String(chars);
+    assertThat(LabelKey.create(key, "").getKey()).isEqualTo(key);
+  }
+
+  @Test
+  public void create_WithUnprintableChars() {
+    String key = "\2ab\3cd";
+    String description = "\4ef\5gh";
+    LabelKey labelKey = LabelKey.create(key, description);
+    assertThat(labelKey.getKey()).isEqualTo(key);
+    assertThat(labelKey.getDescription()).isEqualTo(description);
+  }
+
+  @Test
+  public void create_WithNonAsciiChars() {
+    String key = "é”®";
+    String description = "测试用键";
+    LabelKey nonAsciiKey = LabelKey.create(key, description);
+    assertThat(nonAsciiKey.getKey()).isEqualTo(key);
+    assertThat(nonAsciiKey.getDescription()).isEqualTo(description);
+  }
+
+  @Test
+  public void create_Empty() {
+    LabelKey emptyKey = LabelKey.create("", "");
+    assertThat(emptyKey.getKey()).isEmpty();
+    assertThat(emptyKey.getDescription()).isEmpty();
+  }
+
+  @Test
+  public void testLabelKeyEquals() {
+    new EqualsTester()
+        .addEqualityGroup(LabelKey.create("foo", ""), LabelKey.create("foo", ""))
+        .addEqualityGroup(LabelKey.create("foo", "description"))
+        .addEqualityGroup(LabelKey.create("bar", ""))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/LabelValueTest.java b/api/src/test/java/io/opencensus/metrics/LabelValueTest.java
new file mode 100644
index 0000000..e5526b2
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/LabelValueTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link LabelValue}. */
+@RunWith(JUnit4.class)
+public class LabelValueTest {
+
+  private static final LabelValue VALUE = LabelValue.create("value");
+  private static final LabelValue UNSET = LabelValue.create(null);
+  private static final LabelValue EMPTY = LabelValue.create("");
+
+  @Test
+  public void testGetValue() {
+    assertThat(VALUE.getValue()).isEqualTo("value");
+    assertThat(UNSET.getValue()).isNull();
+    assertThat(EMPTY.getValue()).isEmpty();
+  }
+
+  @Test
+  public void create_NoLengthConstraint() {
+    // We have a length constraint of 256-characters for TagValue. That constraint doesn't apply to
+    // LabelValue.
+    char[] chars = new char[300];
+    Arrays.fill(chars, 'v');
+    String value = new String(chars);
+    assertThat(LabelValue.create(value).getValue()).isEqualTo(value);
+  }
+
+  @Test
+  public void create_WithUnprintableChars() {
+    String value = "\2ab\3cd";
+    assertThat(LabelValue.create(value).getValue()).isEqualTo(value);
+  }
+
+  @Test
+  public void create_WithNonAsciiChars() {
+    String value = "值";
+    LabelValue nonAsciiValue = LabelValue.create(value);
+    assertThat(nonAsciiValue.getValue()).isEqualTo(value);
+  }
+
+  @Test
+  public void testLabelValueEquals() {
+    new EqualsTester()
+        .addEqualityGroup(LabelValue.create("foo"), LabelValue.create("foo"))
+        .addEqualityGroup(UNSET)
+        .addEqualityGroup(EMPTY)
+        .addEqualityGroup(LabelValue.create("bar"))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/LongGaugeTest.java b/api/src/test/java/io/opencensus/metrics/LongGaugeTest.java
new file mode 100644
index 0000000..eedb287
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/LongGaugeTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link LongGauge}. */
+@RunWith(JUnit4.class)
+public class LongGaugeTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String NAME = "name";
+  private static final String DESCRIPTION = "description";
+  private static final String UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+  private static final List<LabelKey> EMPTY_LABEL_KEYS = new ArrayList<LabelKey>();
+  private static final List<LabelValue> EMPTY_LABEL_VALUES = new ArrayList<LabelValue>();
+
+  // TODO(mayurkale): Add more tests, once LongGauge plugs-in into the registry.
+
+  @Test
+  public void noopGetOrCreateTimeSeries_WithNullLabelValues() {
+    LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, EMPTY_LABEL_KEYS);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    longGauge.getOrCreateTimeSeries(null);
+  }
+
+  @Test
+  public void noopGetOrCreateTimeSeries_WithNullElement() {
+    List<LabelValue> labelValues = Collections.singletonList(null);
+    LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValue element should not be null.");
+    longGauge.getOrCreateTimeSeries(labelValues);
+  }
+
+  @Test
+  public void noopGetOrCreateTimeSeries_WithInvalidLabelSize() {
+    LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Incorrect number of labels.");
+    longGauge.getOrCreateTimeSeries(EMPTY_LABEL_VALUES);
+  }
+
+  @Test
+  public void noopRemoveTimeSeries_WithNullLabelValues() {
+    LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    longGauge.removeTimeSeries(null);
+  }
+
+  @Test
+  public void noopSameAs() {
+    LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    assertThat(longGauge.getDefaultTimeSeries()).isSameAs(longGauge.getDefaultTimeSeries());
+    assertThat(longGauge.getDefaultTimeSeries())
+        .isSameAs(longGauge.getOrCreateTimeSeries(LABEL_VALUES));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/MetricRegistryTest.java b/api/src/test/java/io/opencensus/metrics/MetricRegistryTest.java
new file mode 100644
index 0000000..d8a26cc
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/MetricRegistryTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MetricRegistry}. */
+@RunWith(JUnit4.class)
+public class MetricRegistryTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String NAME = "name";
+  private static final String NAME_2 = "name2";
+  private static final String NAME_3 = "name3";
+  private static final String NAME_4 = "name4";
+  private static final String DESCRIPTION = "description";
+  private static final String UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+  private final MetricRegistry metricRegistry =
+      MetricsComponent.newNoopMetricsComponent().getMetricRegistry();
+
+  @Test
+  public void noopAddLongGauge_NullName() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("name");
+    metricRegistry.addLongGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddLongGauge_NullDescription() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("description");
+    metricRegistry.addLongGauge(NAME, null, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddLongGauge_NullUnit() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("unit");
+    metricRegistry.addLongGauge(NAME, DESCRIPTION, null, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddLongGauge_NullLabels() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKeys");
+    metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, null);
+  }
+
+  @Test
+  public void noopAddLongGauge_WithNullElement() {
+    List<LabelKey> labelKeys = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKey element should not be null.");
+    metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, labelKeys);
+  }
+
+  @Test
+  public void noopAddDoubleGauge_NullName() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("name");
+    metricRegistry.addDoubleGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddDoubleGauge_NullDescription() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("description");
+    metricRegistry.addDoubleGauge(NAME_2, null, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddDoubleGauge_NullUnit() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("unit");
+    metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, null, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddDoubleGauge_NullLabels() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKeys");
+    metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, null);
+  }
+
+  @Test
+  public void noopAddDoubleGauge_WithNullElement() {
+    List<LabelKey> labelKeys = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKey element should not be null.");
+    metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, labelKeys);
+  }
+
+  @Test
+  public void noopAddDerivedLongGauge_NullName() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("name");
+    metricRegistry.addDerivedLongGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddDerivedLongGauge_NullDescription() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("description");
+    metricRegistry.addDerivedLongGauge(NAME_3, null, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddDerivedLongGauge_NullUnit() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("unit");
+    metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, null, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddDerivedLongGauge_NullLabels() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKeys");
+    metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, null);
+  }
+
+  @Test
+  public void noopAddDerivedLongGauge_WithNullElement() {
+    List<LabelKey> labelKeys = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKey element should not be null.");
+    metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, labelKeys);
+  }
+
+  @Test
+  public void noopAddDerivedDoubleGauge_NullName() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("name");
+    metricRegistry.addDerivedDoubleGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddDerivedDoubleGauge_NullDescription() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("description");
+    metricRegistry.addDerivedDoubleGauge(NAME_4, null, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddDerivedDoubleGauge_NullUnit() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("unit");
+    metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, null, LABEL_KEY);
+  }
+
+  @Test
+  public void noopAddDerivedDoubleGauge_NullLabels() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKeys");
+    metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, null);
+  }
+
+  @Test
+  public void noopAddDerivedDoubleGauge_WithNullElement() {
+    List<LabelKey> labelKeys = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKey element should not be null.");
+    metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, labelKeys);
+  }
+
+  @Test
+  public void noopSameAs() {
+    LongGauge longGauge = metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    assertThat(longGauge.getDefaultTimeSeries()).isSameAs(longGauge.getDefaultTimeSeries());
+    assertThat(longGauge.getDefaultTimeSeries())
+        .isSameAs(longGauge.getOrCreateTimeSeries(LABEL_VALUES));
+
+    DoubleGauge doubleGauge = metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY);
+    assertThat(doubleGauge.getDefaultTimeSeries()).isSameAs(doubleGauge.getDefaultTimeSeries());
+    assertThat(doubleGauge.getDefaultTimeSeries())
+        .isSameAs(doubleGauge.getOrCreateTimeSeries(LABEL_VALUES));
+  }
+
+  @Test
+  public void noopInstanceOf() {
+    assertThat(metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY))
+        .isInstanceOf(LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY).getClass());
+    assertThat(metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY))
+        .isInstanceOf(
+            DoubleGauge.newNoopDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY).getClass());
+    assertThat(metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY))
+        .isInstanceOf(
+            DerivedLongGauge.newNoopDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY)
+                .getClass());
+    assertThat(metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY))
+        .isInstanceOf(
+            DerivedDoubleGauge.newNoopDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY)
+                .getClass());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/MetricsComponentTest.java b/api/src/test/java/io/opencensus/metrics/MetricsComponentTest.java
new file mode 100644
index 0000000..1c4e70f
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/MetricsComponentTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.metrics.export.ExportComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MetricsComponent}. */
+@RunWith(JUnit4.class)
+public class MetricsComponentTest {
+  @Test
+  public void defaultExportComponent() {
+    assertThat(MetricsComponent.newNoopMetricsComponent().getExportComponent())
+        .isInstanceOf(ExportComponent.newNoopExportComponent().getClass());
+  }
+
+  @Test
+  public void defaultMetricRegistry() {
+    assertThat(MetricsComponent.newNoopMetricsComponent().getMetricRegistry())
+        .isInstanceOf(MetricRegistry.newNoopMetricRegistry().getClass());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/MetricsTest.java b/api/src/test/java/io/opencensus/metrics/MetricsTest.java
new file mode 100644
index 0000000..9e0eee1
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/MetricsTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.metrics.export.ExportComponent;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Metrics}. */
+@RunWith(JUnit4.class)
+public class MetricsTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void loadMetricsComponent_UsesProvidedClassLoader() {
+    final RuntimeException toThrow = new RuntimeException("UseClassLoader");
+    thrown.expect(RuntimeException.class);
+    thrown.expectMessage("UseClassLoader");
+    Metrics.loadMetricsComponent(
+        new ClassLoader() {
+          @Override
+          public Class<?> loadClass(String name) {
+            throw toThrow;
+          }
+        });
+  }
+
+  @Test
+  public void loadMetricsComponent_IgnoresMissingClasses() {
+    ClassLoader classLoader =
+        new ClassLoader() {
+          @Override
+          public Class<?> loadClass(String name) throws ClassNotFoundException {
+            throw new ClassNotFoundException();
+          }
+        };
+    assertThat(Metrics.loadMetricsComponent(classLoader).getClass().getName())
+        .isEqualTo("io.opencensus.metrics.MetricsComponent$NoopMetricsComponent");
+  }
+
+  @Test
+  public void defaultExportComponent() {
+    assertThat(Metrics.getExportComponent())
+        .isInstanceOf(ExportComponent.newNoopExportComponent().getClass());
+  }
+
+  @Test
+  public void defaultMetricRegistry() {
+    assertThat(Metrics.getMetricRegistry())
+        .isInstanceOf(MetricRegistry.newNoopMetricRegistry().getClass());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/DistributionTest.java b/api/src/test/java/io/opencensus/metrics/export/DistributionTest.java
new file mode 100644
index 0000000..85b3149
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/DistributionTest.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.export.Distribution.Bucket;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import io.opencensus.metrics.export.Distribution.BucketOptions.ExplicitOptions;
+import io.opencensus.metrics.export.Distribution.Exemplar;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Distribution}. */
+@RunWith(JUnit4.class)
+public class DistributionTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private static final Timestamp TIMESTAMP = Timestamp.create(1, 0);
+  private static final Map<String, String> ATTACHMENTS = Collections.singletonMap("key", "value");
+  private static final double TOLERANCE = 1e-6;
+
+  @Test
+  public void createAndGet_Bucket() {
+    Bucket bucket = Bucket.create(98);
+    assertThat(bucket.getCount()).isEqualTo(98);
+    assertThat(bucket.getExemplar()).isNull();
+  }
+
+  @Test
+  public void createAndGet_BucketWithExemplar() {
+    Exemplar exemplar = Exemplar.create(12.2, TIMESTAMP, ATTACHMENTS);
+    Bucket bucket = Bucket.create(7, exemplar);
+    assertThat(bucket.getCount()).isEqualTo(7);
+    assertThat(bucket.getExemplar()).isEqualTo(exemplar);
+  }
+
+  @Test
+  public void createBucket_preventNullExemplar() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("exemplar");
+    Bucket.create(1, null);
+  }
+
+  @Test
+  public void createAndGet_Exemplar() {
+    Exemplar exemplar = Exemplar.create(-9.9, TIMESTAMP, ATTACHMENTS);
+    assertThat(exemplar.getValue()).isWithin(TOLERANCE).of(-9.9);
+    assertThat(exemplar.getTimestamp()).isEqualTo(TIMESTAMP);
+    assertThat(exemplar.getAttachments()).isEqualTo(ATTACHMENTS);
+  }
+
+  @Test
+  public void createAndGet_ExplicitBuckets() {
+    List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 3.0);
+
+    BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+    final List<Double> actual = new ArrayList<Double>();
+    bucketOptions.match(
+        new Function<ExplicitOptions, Object>() {
+          @Override
+          public Object apply(ExplicitOptions arg) {
+            actual.addAll(arg.getBucketBoundaries());
+            return null;
+          }
+        },
+        Functions.throwAssertionError());
+
+    assertThat(actual).containsExactlyElementsIn(bucketBounds).inOrder();
+  }
+
+  @Test
+  public void createAndGet_ExplicitBucketsNegativeBounds() {
+    List<Double> bucketBounds = Collections.singletonList(-1.0);
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("bucket boundary should be > 0");
+    BucketOptions.explicitOptions(bucketBounds);
+  }
+
+  @Test
+  public void createAndGet_PreventNullExplicitBuckets() {
+    thrown.expect(NullPointerException.class);
+    BucketOptions.explicitOptions(Arrays.asList(1.0, null, 3.0));
+  }
+
+  @Test
+  public void createAndGet_ExplicitBucketsEmptyBounds() {
+    List<Double> bucketBounds = new ArrayList<Double>();
+    BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+
+    final List<Double> actual = new ArrayList<Double>();
+    bucketOptions.match(
+        new Function<ExplicitOptions, Object>() {
+          @Override
+          public Object apply(ExplicitOptions arg) {
+            actual.addAll(arg.getBucketBoundaries());
+            return null;
+          }
+        },
+        Functions.throwAssertionError());
+
+    assertThat(actual).isEmpty();
+  }
+
+  @Test
+  public void createBucketOptions_UnorderedBucketBounds() {
+    List<Double> bucketBounds = Arrays.asList(1.0, 5.0, 2.0);
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("bucket boundaries not sorted.");
+    BucketOptions.explicitOptions(bucketBounds);
+  }
+
+  @Test
+  public void createAndGet_PreventNullBucketOptions() {
+    thrown.expect(NullPointerException.class);
+    BucketOptions.explicitOptions(null);
+  }
+
+  @Test
+  public void createAndGet_Distribution() {
+    Exemplar exemplar = Exemplar.create(15.0, TIMESTAMP, ATTACHMENTS);
+    List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+    BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+    List<Bucket> buckets =
+        Arrays.asList(
+            Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4, exemplar));
+    Distribution distribution = Distribution.create(10, 6.6, 678.54, bucketOptions, buckets);
+    assertThat(distribution.getCount()).isEqualTo(10);
+    assertThat(distribution.getSum()).isWithin(TOLERANCE).of(6.6);
+    assertThat(distribution.getSumOfSquaredDeviations()).isWithin(TOLERANCE).of(678.54);
+
+    final List<Double> actual = new ArrayList<Double>();
+    distribution
+        .getBucketOptions()
+        .match(
+            new Function<ExplicitOptions, Object>() {
+              @Override
+              public Object apply(ExplicitOptions arg) {
+                actual.addAll(arg.getBucketBoundaries());
+                return null;
+              }
+            },
+            Functions.throwAssertionError());
+
+    assertThat(actual).containsExactlyElementsIn(bucketBounds).inOrder();
+
+    assertThat(distribution.getBuckets()).containsExactlyElementsIn(buckets).inOrder();
+  }
+
+  @Test
+  public void createBucket_NegativeCount() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("bucket count should be non-negative.");
+    Bucket.create(-5);
+  }
+
+  @Test
+  public void createExemplar_PreventNullAttachments() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("attachments");
+    Exemplar.create(15, TIMESTAMP, null);
+  }
+
+  @Test
+  public void createExemplar_PreventNullAttachmentKey() {
+    Map<String, String> attachments = Collections.singletonMap(null, "value");
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("key of attachment");
+    Exemplar.create(15, TIMESTAMP, attachments);
+  }
+
+  @Test
+  public void createExemplar_PreventNullAttachmentValue() {
+    Map<String, String> attachments = Collections.singletonMap("key", null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("value of attachment");
+    Exemplar.create(15, TIMESTAMP, attachments);
+  }
+
+  @Test
+  public void createDistribution_NegativeCount() {
+    List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+    BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+
+    List<Bucket> buckets =
+        Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4));
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("count should be non-negative.");
+    Distribution.create(-10, 6.6, 678.54, bucketOptions, buckets);
+  }
+
+  @Test
+  public void createDistribution_NegativeSumOfSquaredDeviations() {
+    List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+    BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+
+    List<Bucket> buckets =
+        Arrays.asList(Bucket.create(0), Bucket.create(0), Bucket.create(0), Bucket.create(0));
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("sum of squared deviations should be non-negative.");
+    Distribution.create(0, 6.6, -678.54, bucketOptions, buckets);
+  }
+
+  @Test
+  public void createDistribution_ZeroCountAndPositiveMean() {
+    List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+    BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+
+    List<Bucket> buckets =
+        Arrays.asList(Bucket.create(0), Bucket.create(0), Bucket.create(0), Bucket.create(0));
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("sum should be 0 if count is 0.");
+    Distribution.create(0, 6.6, 0, bucketOptions, buckets);
+  }
+
+  @Test
+  public void createDistribution_ZeroCountAndSumOfSquaredDeviations() {
+    List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+    BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+    List<Bucket> buckets =
+        Arrays.asList(Bucket.create(0), Bucket.create(0), Bucket.create(0), Bucket.create(0));
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("sum of squared deviations should be 0 if count is 0.");
+    Distribution.create(0, 0, 678.54, bucketOptions, buckets);
+  }
+
+  @Test
+  public void createDistribution_NullBucketBoundaries() {
+    List<Bucket> buckets =
+        Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4));
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("bucketBoundaries");
+    Distribution.create(10, 6.6, 678.54, BucketOptions.explicitOptions(null), buckets);
+  }
+
+  @Test
+  public void createDistribution_NullBucketBoundary() {
+    List<Bucket> buckets =
+        Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4));
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("bucketBoundary");
+    Distribution.create(
+        10, 6.6, 678.54, BucketOptions.explicitOptions(Arrays.asList(2.5, null)), buckets);
+  }
+
+  @Test
+  public void createDistribution_NullBucketOptions() {
+    List<Bucket> buckets =
+        Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4));
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("bucketOptions");
+    Distribution.create(10, 6.6, 678.54, null, buckets);
+  }
+
+  @Test
+  public void createDistribution_NullBucketList() {
+    List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+    BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("buckets");
+    Distribution.create(10, 6.6, 678.54, bucketOptions, null);
+  }
+
+  @Test
+  public void createDistribution_NullBucket() {
+    List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+    BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+    List<Bucket> buckets =
+        Arrays.asList(Bucket.create(3), Bucket.create(1), null, Bucket.create(4));
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("bucket");
+    Distribution.create(10, 6.6, 678.54, bucketOptions, buckets);
+  }
+
+  @Test
+  public void testEquals() {
+    List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 2.5);
+    new EqualsTester()
+        .addEqualityGroup(
+            Distribution.create(
+                10,
+                10,
+                1,
+                BucketOptions.explicitOptions(bucketBounds),
+                Arrays.asList(
+                    Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4))),
+            Distribution.create(
+                10,
+                10,
+                1,
+                BucketOptions.explicitOptions(bucketBounds),
+                Arrays.asList(
+                    Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4))))
+        .addEqualityGroup(
+            Distribution.create(
+                7,
+                10,
+                23.456,
+                BucketOptions.explicitOptions(bucketBounds),
+                Arrays.asList(
+                    Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4))))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/ExportComponentTest.java b/api/src/test/java/io/opencensus/metrics/export/ExportComponentTest.java
new file mode 100644
index 0000000..15c6e88
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/ExportComponentTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ExportComponent}. */
+@RunWith(JUnit4.class)
+public class ExportComponentTest {
+  @Test
+  public void defaultMetricExporter() {
+    assertThat(ExportComponent.newNoopExportComponent().getMetricProducerManager())
+        .isInstanceOf(MetricProducerManager.class);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/MetricDescriptorTest.java b/api/src/test/java/io/opencensus/metrics/export/MetricDescriptorTest.java
new file mode 100644
index 0000000..502170c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/MetricDescriptorTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import java.util.Arrays;
+import java.util.List;
+import org.hamcrest.CoreMatchers;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MetricDescriptor}. */
+@RunWith(JUnit4.class)
+public class MetricDescriptorTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private static final String METRIC_NAME_1 = "metric1";
+  private static final String METRIC_NAME_2 = "metric2";
+  private static final String DESCRIPTION = "Metric description.";
+  private static final String UNIT = "kb/s";
+  private static final LabelKey KEY_1 = LabelKey.create("key1", "some key");
+  private static final LabelKey KEY_2 = LabelKey.create("key2", "some other key");
+
+  @Test
+  public void testGet() {
+    MetricDescriptor metricDescriptor =
+        MetricDescriptor.create(
+            METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2));
+    assertThat(metricDescriptor.getName()).isEqualTo(METRIC_NAME_1);
+    assertThat(metricDescriptor.getDescription()).isEqualTo(DESCRIPTION);
+    assertThat(metricDescriptor.getUnit()).isEqualTo(UNIT);
+    assertThat(metricDescriptor.getType()).isEqualTo(Type.GAUGE_DOUBLE);
+    assertThat(metricDescriptor.getLabelKeys()).containsExactly(KEY_1, KEY_2).inOrder();
+  }
+
+  @Test
+  public void preventNullLabelKeyList() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(CoreMatchers.equalTo("labelKeys"));
+    MetricDescriptor.create(METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, null);
+  }
+
+  @Test
+  public void preventNullLabelKey() {
+    List<LabelKey> keys = Arrays.asList(KEY_1, null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(CoreMatchers.equalTo("labelKey"));
+    MetricDescriptor.create(METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, keys);
+  }
+
+  @Test
+  public void testEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            MetricDescriptor.create(
+                METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2)),
+            MetricDescriptor.create(
+                METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2)))
+        .addEqualityGroup(
+            MetricDescriptor.create(
+                METRIC_NAME_2, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2)))
+        .addEqualityGroup(
+            MetricDescriptor.create(
+                METRIC_NAME_2, DESCRIPTION, UNIT, Type.GAUGE_INT64, Arrays.asList(KEY_1, KEY_2)))
+        .addEqualityGroup(
+            MetricDescriptor.create(
+                METRIC_NAME_1,
+                DESCRIPTION,
+                UNIT,
+                Type.CUMULATIVE_DISTRIBUTION,
+                Arrays.asList(KEY_1, KEY_2)))
+        .addEqualityGroup(
+            MetricDescriptor.create(
+                METRIC_NAME_1,
+                DESCRIPTION,
+                UNIT,
+                Type.CUMULATIVE_DISTRIBUTION,
+                Arrays.asList(KEY_1)))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/MetricProducerManagerTest.java b/api/src/test/java/io/opencensus/metrics/export/MetricProducerManagerTest.java
new file mode 100644
index 0000000..1025427
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/MetricProducerManagerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link MetricProducerManager}. */
+@RunWith(JUnit4.class)
+public class MetricProducerManagerTest {
+  private final MetricProducerManager metricProducerManager =
+      MetricProducerManager.newNoopMetricProducerManager();
+  @Mock private MetricProducer metricProducer;
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void add_DisallowsNull() {
+    thrown.expect(NullPointerException.class);
+    metricProducerManager.add(null);
+  }
+
+  @Test
+  public void add() {
+    metricProducerManager.add(metricProducer);
+    assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+  }
+
+  @Test
+  public void addAndRemove() {
+    metricProducerManager.add(metricProducer);
+    assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+    metricProducerManager.remove(metricProducer);
+    assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+  }
+
+  @Test
+  public void remove_DisallowsNull() {
+    thrown.expect(NullPointerException.class);
+    metricProducerManager.remove(null);
+  }
+
+  @Test
+  public void remove_FromEmpty() {
+    metricProducerManager.remove(metricProducer);
+    assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+  }
+
+  @Test
+  public void getAllMetricProducer_empty() {
+    assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/MetricTest.java b/api/src/test/java/io/opencensus/metrics/export/MetricTest.java
new file mode 100644
index 0000000..ed20528
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/MetricTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Metric}. */
+@RunWith(JUnit4.class)
+public class MetricTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private static final String METRIC_NAME_1 = "metric1";
+  private static final String METRIC_NAME_2 = "metric2";
+  private static final String DESCRIPTION = "Metric description.";
+  private static final String UNIT = "kb/s";
+  private static final LabelKey KEY_1 = LabelKey.create("key1", "some key");
+  private static final LabelKey KEY_2 = LabelKey.create("key1", "some other key");
+  private static final MetricDescriptor METRIC_DESCRIPTOR_1 =
+      MetricDescriptor.create(
+          METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2));
+  private static final MetricDescriptor METRIC_DESCRIPTOR_2 =
+      MetricDescriptor.create(
+          METRIC_NAME_2,
+          DESCRIPTION,
+          UNIT,
+          Type.CUMULATIVE_INT64,
+          Collections.singletonList(KEY_1));
+  private static final LabelValue LABEL_VALUE_1 = LabelValue.create("value1");
+  private static final LabelValue LABEL_VALUE_2 = LabelValue.create("value1");
+  private static final LabelValue LABEL_VALUE_EMPTY = LabelValue.create("");
+  private static final Value VALUE_LONG = Value.longValue(12345678);
+  private static final Value VALUE_DOUBLE_1 = Value.doubleValue(-345.77);
+  private static final Value VALUE_DOUBLE_2 = Value.doubleValue(133.79);
+  private static final Timestamp TIMESTAMP_1 = Timestamp.fromMillis(1000);
+  private static final Timestamp TIMESTAMP_2 = Timestamp.fromMillis(2000);
+  private static final Timestamp TIMESTAMP_3 = Timestamp.fromMillis(3000);
+  private static final Point POINT_1 = Point.create(VALUE_DOUBLE_1, TIMESTAMP_2);
+  private static final Point POINT_2 = Point.create(VALUE_DOUBLE_2, TIMESTAMP_3);
+  private static final Point POINT_3 = Point.create(VALUE_LONG, TIMESTAMP_3);
+  private static final TimeSeries GAUGE_TIME_SERIES_1 =
+      TimeSeries.create(
+          Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Collections.singletonList(POINT_1), null);
+  private static final TimeSeries GAUGE_TIME_SERIES_2 =
+      TimeSeries.create(
+          Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Collections.singletonList(POINT_2), null);
+  private static final TimeSeries CUMULATIVE_TIME_SERIES =
+      TimeSeries.create(
+          Collections.singletonList(LABEL_VALUE_EMPTY),
+          Collections.singletonList(POINT_3),
+          TIMESTAMP_1);
+
+  @Test
+  public void testGet() {
+    Metric metric =
+        Metric.create(METRIC_DESCRIPTOR_1, Arrays.asList(GAUGE_TIME_SERIES_1, GAUGE_TIME_SERIES_2));
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR_1);
+    assertThat(metric.getTimeSeriesList())
+        .containsExactly(GAUGE_TIME_SERIES_1, GAUGE_TIME_SERIES_2)
+        .inOrder();
+  }
+
+  @Test
+  public void typeMismatch_GaugeDouble_Long() {
+    typeMismatch(
+        METRIC_DESCRIPTOR_1,
+        Collections.singletonList(CUMULATIVE_TIME_SERIES),
+        String.format("Type mismatch: %s, %s.", Type.GAUGE_DOUBLE, "ValueLong"));
+  }
+
+  @Test
+  public void typeMismatch_CumulativeInt64_Double() {
+    typeMismatch(
+        METRIC_DESCRIPTOR_2,
+        Collections.singletonList(GAUGE_TIME_SERIES_1),
+        String.format("Type mismatch: %s, %s.", Type.CUMULATIVE_INT64, "ValueDouble"));
+  }
+
+  private void typeMismatch(
+      MetricDescriptor metricDescriptor, List<TimeSeries> timeSeriesList, String errorMessage) {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage(errorMessage);
+    Metric.create(metricDescriptor, timeSeriesList);
+  }
+
+  @Test
+  public void create_WithNullMetricDescriptor() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("metricDescriptor");
+    Metric.create(null, Collections.<TimeSeries>emptyList());
+  }
+
+  @Test
+  public void create_WithNullTimeSeriesList() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("timeSeriesList");
+    Metric.create(METRIC_DESCRIPTOR_1, null);
+  }
+
+  @Test
+  public void create_WithNullTimeSeries() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("timeSeries");
+    Metric.create(METRIC_DESCRIPTOR_1, Arrays.asList(GAUGE_TIME_SERIES_1, null));
+  }
+
+  @Test
+  public void immutableTimeSeriesList() {
+    List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>();
+    timeSeriesList.add(GAUGE_TIME_SERIES_1);
+    Metric metric = Metric.create(METRIC_DESCRIPTOR_1, timeSeriesList);
+    timeSeriesList.add(GAUGE_TIME_SERIES_2);
+    assertThat(metric.getTimeSeriesList()).containsExactly(GAUGE_TIME_SERIES_1);
+  }
+
+  @Test
+  public void createWithOneTimeSeries_WithNullTimeSeries() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("timeSeries");
+    Metric.createWithOneTimeSeries(METRIC_DESCRIPTOR_1, null);
+  }
+
+  @Test
+  public void createWithOneTimeSeries_WithNullMetricDescriptor() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("metricDescriptor");
+    Metric.createWithOneTimeSeries(null, GAUGE_TIME_SERIES_1);
+  }
+
+  @Test
+  public void testGet_WithOneTimeSeries() {
+    Metric metric = Metric.createWithOneTimeSeries(METRIC_DESCRIPTOR_1, GAUGE_TIME_SERIES_1);
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR_1);
+    assertThat(metric.getTimeSeriesList()).containsExactly(GAUGE_TIME_SERIES_1);
+  }
+
+  @Test
+  public void testEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            Metric.create(
+                METRIC_DESCRIPTOR_1, Arrays.asList(GAUGE_TIME_SERIES_1, GAUGE_TIME_SERIES_2)),
+            Metric.create(
+                METRIC_DESCRIPTOR_1, Arrays.asList(GAUGE_TIME_SERIES_1, GAUGE_TIME_SERIES_2)))
+        .addEqualityGroup(Metric.create(METRIC_DESCRIPTOR_1, Collections.<TimeSeries>emptyList()))
+        .addEqualityGroup(
+            Metric.createWithOneTimeSeries(METRIC_DESCRIPTOR_2, CUMULATIVE_TIME_SERIES))
+        .addEqualityGroup(Metric.create(METRIC_DESCRIPTOR_2, Collections.<TimeSeries>emptyList()))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/PointTest.java b/api/src/test/java/io/opencensus/metrics/export/PointTest.java
new file mode 100644
index 0000000..cdfc779
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/PointTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.export.Distribution.Bucket;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Point}. */
+@RunWith(JUnit4.class)
+public class PointTest {
+
+  private static final Value DOUBLE_VALUE = Value.doubleValue(55.5);
+  private static final Value LONG_VALUE = Value.longValue(9876543210L);
+  private static final Value DISTRIBUTION_VALUE =
+      Value.distributionValue(
+          Distribution.create(
+              10,
+              6.6,
+              678.54,
+              BucketOptions.explicitOptions(Arrays.asList(1.0, 2.0, 5.0)),
+              Arrays.asList(
+                  Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4))));
+  private static final Timestamp TIMESTAMP_1 = Timestamp.create(1, 2);
+  private static final Timestamp TIMESTAMP_2 = Timestamp.create(3, 4);
+  private static final Timestamp TIMESTAMP_3 = Timestamp.create(5, 6);
+
+  @Test
+  public void testGet() {
+    Point point = Point.create(DOUBLE_VALUE, TIMESTAMP_1);
+    assertThat(point.getValue()).isEqualTo(DOUBLE_VALUE);
+    assertThat(point.getTimestamp()).isEqualTo(TIMESTAMP_1);
+  }
+
+  @Test
+  public void testEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            Point.create(DOUBLE_VALUE, TIMESTAMP_1), Point.create(DOUBLE_VALUE, TIMESTAMP_1))
+        .addEqualityGroup(Point.create(LONG_VALUE, TIMESTAMP_1))
+        .addEqualityGroup(Point.create(LONG_VALUE, TIMESTAMP_2))
+        .addEqualityGroup(
+            Point.create(DISTRIBUTION_VALUE, TIMESTAMP_2),
+            Point.create(DISTRIBUTION_VALUE, TIMESTAMP_2))
+        .addEqualityGroup(Point.create(DISTRIBUTION_VALUE, TIMESTAMP_3))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/SummaryTest.java b/api/src/test/java/io/opencensus/metrics/export/SummaryTest.java
new file mode 100644
index 0000000..c10df04
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/SummaryTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.metrics.export.Summary.Snapshot;
+import io.opencensus.metrics.export.Summary.Snapshot.ValueAtPercentile;
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Summary}. */
+@RunWith(JUnit4.class)
+public class SummaryTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+  private static final double TOLERANCE = 1e-6;
+
+  @Test
+  public void createAndGet_ValueAtPercentile() {
+    ValueAtPercentile valueAtPercentile = ValueAtPercentile.create(99.5, 10.2);
+    assertThat(valueAtPercentile.getPercentile()).isWithin(TOLERANCE).of(99.5);
+    assertThat(valueAtPercentile.getValue()).isWithin(TOLERANCE).of(10.2);
+  }
+
+  @Test
+  public void createValueAtPercentile_InvalidValueAtPercentileInterval() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("percentile must be in the interval (0.0, 100.0]");
+    ValueAtPercentile.create(100.1, 10.2);
+  }
+
+  @Test
+  public void createValueAtPercentile_NegativeValue() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("value must be non-negative");
+    ValueAtPercentile.create(99.5, -10.2);
+  }
+
+  @Test
+  public void createAndGet_Snapshot() {
+    Snapshot snapshot =
+        Snapshot.create(
+            10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+    assertThat(snapshot.getCount()).isEqualTo(10);
+    assertThat(snapshot.getSum()).isWithin(TOLERANCE).of(87.07);
+    assertThat(snapshot.getValueAtPercentiles())
+        .containsExactly(ValueAtPercentile.create(99.5, 10.2));
+  }
+
+  @Test
+  public void createAndGet_Snapshot_WithNullCountAndSum() {
+    Snapshot snapshot =
+        Snapshot.create(
+            null, null, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+    assertThat(snapshot.getCount()).isNull();
+    assertThat(snapshot.getSum()).isNull();
+    assertThat(snapshot.getValueAtPercentiles())
+        .containsExactly(ValueAtPercentile.create(99.5, 10.2));
+  }
+
+  @Test
+  public void createSnapshot_NegativeCount() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("count must be non-negative");
+    Snapshot.create(-10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+  }
+
+  @Test
+  public void createSnapshot_NegativeSum() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("sum must be non-negative");
+    Snapshot.create(10L, -87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+  }
+
+  @Test
+  public void createSnapshot_ZeroCountAndNonZeroSum() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("sum must be 0 if count is 0");
+    Snapshot.create(0L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+  }
+
+  @Test
+  public void createSnapshot_NullValueAtPercentilesList() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("valueAtPercentiles");
+    Snapshot.create(10L, 87.07, null);
+  }
+
+  @Test
+  public void createSnapshot_OneNullValueAtPercentile() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("value in valueAtPercentiles");
+    Snapshot.create(10L, 87.07, Collections.<ValueAtPercentile>singletonList(null));
+  }
+
+  @Test
+  public void createAndGet_Summary() {
+    Snapshot snapshot =
+        Snapshot.create(
+            10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+    Summary summary = Summary.create(10L, 6.6, snapshot);
+    assertThat(summary.getCount()).isEqualTo(10);
+    assertThat(summary.getSum()).isWithin(TOLERANCE).of(6.6);
+    assertThat(summary.getSnapshot()).isEqualTo(snapshot);
+  }
+
+  @Test
+  public void createSummary_NegativeCount() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("count must be non-negative");
+    Summary.create(
+        -10L, 6.6, Snapshot.create(null, null, Collections.<ValueAtPercentile>emptyList()));
+  }
+
+  @Test
+  public void createSummary_NegativeSum() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("sum must be non-negative");
+    Summary.create(
+        10L, -6.6, Snapshot.create(null, null, Collections.<ValueAtPercentile>emptyList()));
+  }
+
+  @Test
+  public void createSummary_ZeroCountAndNonZeroSum() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("sum must be 0 if count is 0");
+    Summary.create(
+        0L, 6.6, Snapshot.create(null, null, Collections.<ValueAtPercentile>emptyList()));
+  }
+
+  @Test
+  public void createSummary_NullSnapshot() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("snapshot");
+    Summary.create(10L, 6.6, null);
+  }
+
+  @Test
+  public void testEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            Summary.create(
+                10L,
+                10.0,
+                Snapshot.create(
+                    10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)))),
+            Summary.create(
+                10L,
+                10.0,
+                Snapshot.create(
+                    10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)))))
+        .addEqualityGroup(
+            Summary.create(
+                7L,
+                10.0,
+                Snapshot.create(
+                    10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)))))
+        .addEqualityGroup(
+            Summary.create(
+                10L,
+                7.0,
+                Snapshot.create(
+                    10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)))))
+        .addEqualityGroup(
+            Summary.create(
+                10L, 10.0, Snapshot.create(null, null, Collections.<ValueAtPercentile>emptyList())))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/TimeSeriesTest.java b/api/src/test/java/io/opencensus/metrics/export/TimeSeriesTest.java
new file mode 100644
index 0000000..92a2c8c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/TimeSeriesTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.LabelValue;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.hamcrest.CoreMatchers;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TimeSeries}. */
+@RunWith(JUnit4.class)
+public class TimeSeriesTest {
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final LabelValue LABEL_VALUE_1 = LabelValue.create("value1");
+  private static final LabelValue LABEL_VALUE_2 = LabelValue.create("value2");
+  private static final Value VALUE_LONG = Value.longValue(12345678);
+  private static final Value VALUE_DOUBLE = Value.doubleValue(-345.77);
+  private static final Timestamp TIMESTAMP_1 = Timestamp.fromMillis(1000);
+  private static final Timestamp TIMESTAMP_2 = Timestamp.fromMillis(2000);
+  private static final Timestamp TIMESTAMP_3 = Timestamp.fromMillis(3000);
+  private static final Point POINT_1 = Point.create(VALUE_DOUBLE, TIMESTAMP_2);
+  private static final Point POINT_2 = Point.create(VALUE_LONG, TIMESTAMP_3);
+
+  @Test
+  public void testGet_TimeSeries() {
+    TimeSeries cumulativeTimeSeries =
+        TimeSeries.create(
+            Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), TIMESTAMP_1);
+    assertThat(cumulativeTimeSeries.getStartTimestamp()).isEqualTo(TIMESTAMP_1);
+    assertThat(cumulativeTimeSeries.getLabelValues())
+        .containsExactly(LABEL_VALUE_1, LABEL_VALUE_2)
+        .inOrder();
+    assertThat(cumulativeTimeSeries.getPoints()).containsExactly(POINT_1).inOrder();
+  }
+
+  @Test
+  public void create_WithNullLabelValueList() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(CoreMatchers.equalTo("labelValues"));
+    TimeSeries.create(null, Collections.<Point>emptyList(), TIMESTAMP_1);
+  }
+
+  @Test
+  public void create_WithNullLabelValue() {
+    List<LabelValue> labelValues = Arrays.asList(LABEL_VALUE_1, null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(CoreMatchers.equalTo("labelValue"));
+    TimeSeries.create(labelValues, Collections.<Point>emptyList(), TIMESTAMP_1);
+  }
+
+  @Test
+  public void create_WithNullPointList() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(CoreMatchers.equalTo("points"));
+    TimeSeries.create(Collections.<LabelValue>emptyList(), null, TIMESTAMP_1);
+  }
+
+  @Test
+  public void create_WithNullPoint() {
+    List<Point> points = Arrays.asList(POINT_1, null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(CoreMatchers.equalTo("point"));
+    TimeSeries.create(Collections.<LabelValue>emptyList(), points, TIMESTAMP_1);
+  }
+
+  @Test
+  public void testGet_WithOnePointTimeSeries() {
+    TimeSeries cumulativeTimeSeries =
+        TimeSeries.createWithOnePoint(
+            Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), POINT_1, TIMESTAMP_1);
+    assertThat(cumulativeTimeSeries.getStartTimestamp()).isEqualTo(TIMESTAMP_1);
+    assertThat(cumulativeTimeSeries.getLabelValues())
+        .containsExactly(LABEL_VALUE_1, LABEL_VALUE_2)
+        .inOrder();
+    assertThat(cumulativeTimeSeries.getPoints()).containsExactly(POINT_1).inOrder();
+  }
+
+  @Test
+  public void createWithOnePoint_WithNullLabelValueList() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(CoreMatchers.equalTo("labelValues"));
+    TimeSeries.createWithOnePoint(null, POINT_1, TIMESTAMP_1);
+  }
+
+  @Test
+  public void createWithOnePoint_WithNullLabelValue() {
+    List<LabelValue> labelValues = Arrays.asList(LABEL_VALUE_1, null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(CoreMatchers.equalTo("labelValue"));
+    TimeSeries.createWithOnePoint(labelValues, POINT_1, TIMESTAMP_1);
+  }
+
+  @Test
+  public void createWithOnePoint_WithNullPointList() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage(CoreMatchers.equalTo("point"));
+    TimeSeries.createWithOnePoint(Collections.<LabelValue>emptyList(), null, TIMESTAMP_1);
+  }
+
+  @Test
+  public void testEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            TimeSeries.create(
+                Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), TIMESTAMP_1),
+            TimeSeries.create(
+                Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), TIMESTAMP_1))
+        .addEqualityGroup(
+            TimeSeries.create(
+                Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), null),
+            TimeSeries.create(
+                Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), null))
+        .addEqualityGroup(
+            TimeSeries.create(
+                Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), TIMESTAMP_2))
+        .addEqualityGroup(
+            TimeSeries.create(Arrays.asList(LABEL_VALUE_1), Arrays.asList(POINT_1), TIMESTAMP_2))
+        .addEqualityGroup(
+            TimeSeries.create(Arrays.asList(LABEL_VALUE_1), Arrays.asList(POINT_2), TIMESTAMP_2))
+        .addEqualityGroup(
+            TimeSeries.create(
+                Arrays.asList(LABEL_VALUE_1), Arrays.asList(POINT_1, POINT_2), TIMESTAMP_2))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/ValueTest.java b/api/src/test/java/io/opencensus/metrics/export/ValueTest.java
new file mode 100644
index 0000000..bf94769
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/ValueTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.metrics.export.Distribution.Bucket;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import io.opencensus.metrics.export.Distribution.BucketOptions.ExplicitOptions;
+import io.opencensus.metrics.export.Summary.Snapshot;
+import io.opencensus.metrics.export.Summary.Snapshot.ValueAtPercentile;
+import io.opencensus.metrics.export.Value.ValueDistribution;
+import io.opencensus.metrics.export.Value.ValueDouble;
+import io.opencensus.metrics.export.Value.ValueLong;
+import io.opencensus.metrics.export.Value.ValueSummary;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Value}. */
+@RunWith(JUnit4.class)
+public class ValueTest {
+  private static final double TOLERANCE = 1e-6;
+
+  private static final Distribution DISTRIBUTION =
+      Distribution.create(
+          10,
+          10,
+          1,
+          BucketOptions.explicitOptions(Arrays.asList(1.0, 2.0, 5.0)),
+          Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4)));
+  private static final Summary SUMMARY =
+      Summary.create(
+          10L,
+          10.0,
+          Snapshot.create(
+              10L, 87.07, Collections.singletonList(ValueAtPercentile.create(0.98, 10.2))));
+
+  @Test
+  public void createAndGet_ValueDouble() {
+    Value value = Value.doubleValue(-34.56);
+    assertThat(value).isInstanceOf(ValueDouble.class);
+    assertThat(((ValueDouble) value).getValue()).isWithin(TOLERANCE).of(-34.56);
+  }
+
+  @Test
+  public void createAndGet_ValueLong() {
+    Value value = Value.longValue(123456789);
+    assertThat(value).isInstanceOf(ValueLong.class);
+    assertThat(((ValueLong) value).getValue()).isEqualTo(123456789);
+  }
+
+  @Test
+  public void createAndGet_ValueDistribution() {
+    Value value = Value.distributionValue(DISTRIBUTION);
+    assertThat(value).isInstanceOf(ValueDistribution.class);
+    assertThat(((ValueDistribution) value).getValue()).isEqualTo(DISTRIBUTION);
+  }
+
+  @Test
+  public void createAndGet_ValueSummary() {
+    Value value = Value.summaryValue(SUMMARY);
+    assertThat(value).isInstanceOf(ValueSummary.class);
+    assertThat(((ValueSummary) value).getValue()).isEqualTo(SUMMARY);
+  }
+
+  @Test
+  public void testEquals() {
+    new EqualsTester()
+        .addEqualityGroup(Value.doubleValue(1.0), Value.doubleValue(1.0))
+        .addEqualityGroup(Value.doubleValue(2.0))
+        .addEqualityGroup(Value.longValue(1L))
+        .addEqualityGroup(Value.longValue(2L))
+        .addEqualityGroup(
+            Value.distributionValue(
+                Distribution.create(
+                    7,
+                    10,
+                    23.456,
+                    BucketOptions.explicitOptions(Arrays.asList(1.0, 2.0, 5.0)),
+                    Arrays.asList(
+                        Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4)))))
+        .testEquals();
+  }
+
+  @Test
+  public void testMatch() {
+    List<Value> values =
+        Arrays.asList(
+            ValueDouble.create(1.0),
+            ValueLong.create(-1),
+            ValueDistribution.create(DISTRIBUTION),
+            ValueSummary.create(SUMMARY));
+    List<Number> expected =
+        Arrays.<Number>asList(1.0, -1L, 10.0, 10L, 1.0, 1.0, 2.0, 5.0, 3L, 1L, 2L, 4L);
+    final List<Number> actual = new ArrayList<Number>();
+    for (Value value : values) {
+      value.match(
+          new Function<Double, Object>() {
+            @Override
+            public Object apply(Double arg) {
+              actual.add(arg);
+              return null;
+            }
+          },
+          new Function<Long, Object>() {
+            @Override
+            public Object apply(Long arg) {
+              actual.add(arg);
+              return null;
+            }
+          },
+          new Function<Distribution, Object>() {
+            @Override
+            public Object apply(Distribution arg) {
+              actual.add(arg.getSum());
+              actual.add(arg.getCount());
+              actual.add(arg.getSumOfSquaredDeviations());
+
+              arg.getBucketOptions()
+                  .match(
+                      new Function<ExplicitOptions, Object>() {
+                        @Override
+                        public Object apply(ExplicitOptions arg) {
+                          actual.addAll(arg.getBucketBoundaries());
+                          return null;
+                        }
+                      },
+                      Functions.throwAssertionError());
+
+              for (Bucket bucket : arg.getBuckets()) {
+                actual.add(bucket.getCount());
+              }
+              return null;
+            }
+          },
+          new Function<Summary, Object>() {
+            @Override
+            public Object apply(Summary arg) {
+              return null;
+            }
+          },
+          Functions.throwAssertionError());
+    }
+    assertThat(actual).containsExactlyElementsIn(expected).inOrder();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/stats/AggregationDataTest.java b/api/src/test/java/io/opencensus/stats/AggregationDataTest.java
new file mode 100644
index 0000000..a6d6d1d
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/AggregationDataTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link io.opencensus.stats.AggregationData}. */
+@RunWith(JUnit4.class)
+public class AggregationDataTest {
+
+  private static final double TOLERANCE = 1e-6;
+  private static final Timestamp TIMESTAMP_1 = Timestamp.create(1, 0);
+  private static final Timestamp TIMESTAMP_2 = Timestamp.create(2, 0);
+  private static final Map<String, String> ATTACHMENTS = Collections.singletonMap("key", "value");
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testCreateDistributionData() {
+    DistributionData distributionData =
+        DistributionData.create(7.7, 10, 1.1, 9.9, 32.2, Arrays.asList(4L, 1L, 5L));
+    assertThat(distributionData.getMean()).isWithin(TOLERANCE).of(7.7);
+    assertThat(distributionData.getCount()).isEqualTo(10);
+    assertThat(distributionData.getMin()).isWithin(TOLERANCE).of(1.1);
+    assertThat(distributionData.getMax()).isWithin(TOLERANCE).of(9.9);
+    assertThat(distributionData.getSumOfSquaredDeviations()).isWithin(TOLERANCE).of(32.2);
+    assertThat(distributionData.getBucketCounts()).containsExactly(4L, 1L, 5L).inOrder();
+  }
+
+  @Test
+  public void testCreateDistributionDataWithExemplar() {
+    Exemplar exemplar1 = Exemplar.create(4, TIMESTAMP_2, ATTACHMENTS);
+    Exemplar exemplar2 = Exemplar.create(1, TIMESTAMP_1, ATTACHMENTS);
+    DistributionData distributionData =
+        DistributionData.create(
+            7.7, 10, 1.1, 9.9, 32.2, Arrays.asList(4L, 1L), Arrays.asList(exemplar1, exemplar2));
+    assertThat(distributionData.getExemplars()).containsExactly(exemplar1, exemplar2).inOrder();
+  }
+
+  @Test
+  public void testExemplar() {
+    Exemplar exemplar = Exemplar.create(15.0, TIMESTAMP_1, ATTACHMENTS);
+    assertThat(exemplar.getValue()).isEqualTo(15.0);
+    assertThat(exemplar.getTimestamp()).isEqualTo(TIMESTAMP_1);
+    assertThat(exemplar.getAttachments()).isEqualTo(ATTACHMENTS);
+  }
+
+  @Test
+  public void testExemplar_PreventNullAttachments() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("attachments");
+    Exemplar.create(15, TIMESTAMP_1, null);
+  }
+
+  @Test
+  public void testExemplar_PreventNullAttachmentKey() {
+    Map<String, String> attachments = Collections.singletonMap(null, "value");
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("key of attachment");
+    Exemplar.create(15, TIMESTAMP_1, attachments);
+  }
+
+  @Test
+  public void testExemplar_PreventNullAttachmentValue() {
+    Map<String, String> attachments = Collections.singletonMap("key", null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("value of attachment");
+    Exemplar.create(15, TIMESTAMP_1, attachments);
+  }
+
+  @Test
+  public void preventNullBucketCountList() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("bucketCounts");
+    DistributionData.create(1, 1, 1, 1, 0, null);
+  }
+
+  @Test
+  public void preventNullBucket() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("bucket");
+    DistributionData.create(1, 1, 1, 1, 0, Arrays.asList(0L, 1L, null));
+  }
+
+  @Test
+  public void preventNullExemplarList() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("exemplar list should not be null.");
+    DistributionData.create(1, 1, 1, 1, 0, Arrays.asList(0L, 1L, 1L), null);
+  }
+
+  @Test
+  public void preventNullExemplar() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("exemplar");
+    DistributionData.create(
+        1, 1, 1, 1, 0, Arrays.asList(0L, 1L, 1L), Collections.<Exemplar>singletonList(null));
+  }
+
+  @Test
+  public void preventMinIsGreaterThanMax() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("max should be greater or equal to min.");
+    DistributionData.create(1, 1, 10, 1, 0, Arrays.asList(0L, 1L, 0L));
+  }
+
+  @Test
+  public void testEquals() {
+    new EqualsTester()
+        .addEqualityGroup(SumDataDouble.create(10.0), SumDataDouble.create(10.0))
+        .addEqualityGroup(SumDataDouble.create(20.0), SumDataDouble.create(20.0))
+        .addEqualityGroup(SumDataLong.create(20), SumDataLong.create(20))
+        .addEqualityGroup(CountData.create(40), CountData.create(40))
+        .addEqualityGroup(CountData.create(80), CountData.create(80))
+        .addEqualityGroup(
+            DistributionData.create(10, 10, 1, 1, 0, Arrays.asList(0L, 10L, 0L)),
+            DistributionData.create(10, 10, 1, 1, 0, Arrays.asList(0L, 10L, 0L)))
+        .addEqualityGroup(DistributionData.create(10, 10, 1, 1, 0, Arrays.asList(0L, 10L, 100L)))
+        .addEqualityGroup(DistributionData.create(110, 10, 1, 1, 0, Arrays.asList(0L, 10L, 0L)))
+        .addEqualityGroup(DistributionData.create(10, 110, 1, 1, 0, Arrays.asList(0L, 10L, 0L)))
+        .addEqualityGroup(DistributionData.create(10, 10, -1, 1, 0, Arrays.asList(0L, 10L, 0L)))
+        .addEqualityGroup(DistributionData.create(10, 10, 1, 5, 0, Arrays.asList(0L, 10L, 0L)))
+        .addEqualityGroup(DistributionData.create(10, 10, 1, 1, 55.5, Arrays.asList(0L, 10L, 0L)))
+        .addEqualityGroup(MeanData.create(5.0, 1), MeanData.create(5.0, 1))
+        .addEqualityGroup(MeanData.create(-5.0, 1), MeanData.create(-5.0, 1))
+        .addEqualityGroup(LastValueDataDouble.create(20.0), LastValueDataDouble.create(20.0))
+        .addEqualityGroup(LastValueDataLong.create(20), LastValueDataLong.create(20))
+        .testEquals();
+  }
+
+  @Test
+  public void testMatchAndGet() {
+    List<AggregationData> aggregations =
+        Arrays.asList(
+            SumDataDouble.create(10.0),
+            SumDataLong.create(100000000),
+            CountData.create(40),
+            DistributionData.create(1, 1, 1, 1, 0, Arrays.asList(0L, 10L, 0L)),
+            LastValueDataDouble.create(20.0),
+            LastValueDataLong.create(200000000L));
+
+    final List<Object> actual = new ArrayList<Object>();
+    for (AggregationData aggregation : aggregations) {
+      aggregation.match(
+          new Function<SumDataDouble, Void>() {
+            @Override
+            public Void apply(SumDataDouble arg) {
+              actual.add(arg.getSum());
+              return null;
+            }
+          },
+          new Function<SumDataLong, Void>() {
+            @Override
+            public Void apply(SumDataLong arg) {
+              actual.add(arg.getSum());
+              return null;
+            }
+          },
+          new Function<CountData, Void>() {
+            @Override
+            public Void apply(CountData arg) {
+              actual.add(arg.getCount());
+              return null;
+            }
+          },
+          new Function<DistributionData, Void>() {
+            @Override
+            public Void apply(DistributionData arg) {
+              actual.add(arg.getBucketCounts());
+              return null;
+            }
+          },
+          new Function<LastValueDataDouble, Void>() {
+            @Override
+            public Void apply(LastValueDataDouble arg) {
+              actual.add(arg.getLastValue());
+              return null;
+            }
+          },
+          new Function<LastValueDataLong, Void>() {
+            @Override
+            public Void apply(LastValueDataLong arg) {
+              actual.add(arg.getLastValue());
+              return null;
+            }
+          },
+          Functions.<Void>throwIllegalArgumentException());
+    }
+
+    assertThat(actual)
+        .containsExactly(10.0, 100000000L, 40L, Arrays.asList(0L, 10L, 0L), 20.0, 200000000L)
+        .inOrder();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/stats/AggregationTest.java b/api/src/test/java/io/opencensus/stats/AggregationTest.java
new file mode 100644
index 0000000..cf33703
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/AggregationTest.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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Functions;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link io.opencensus.stats.Aggregation}. */
+@RunWith(JUnit4.class)
+public class AggregationTest {
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testCreateDistribution() {
+    BucketBoundaries bucketBoundaries = BucketBoundaries.create(Arrays.asList(0.1, 2.2, 33.3));
+    Distribution distribution = Distribution.create(bucketBoundaries);
+    assertThat(distribution.getBucketBoundaries()).isEqualTo(bucketBoundaries);
+  }
+
+  @Test
+  public void testNullBucketBoundaries() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("bucketBoundaries");
+    Distribution.create(null);
+  }
+
+  @Test
+  public void testEquals() {
+    new EqualsTester()
+        .addEqualityGroup(Sum.create(), Sum.create())
+        .addEqualityGroup(Count.create(), Count.create())
+        .addEqualityGroup(
+            Distribution.create(BucketBoundaries.create(Arrays.asList(-10.0, 1.0, 5.0))),
+            Distribution.create(BucketBoundaries.create(Arrays.asList(-10.0, 1.0, 5.0))))
+        .addEqualityGroup(
+            Distribution.create(BucketBoundaries.create(Arrays.asList(0.0, 1.0, 5.0))),
+            Distribution.create(BucketBoundaries.create(Arrays.asList(0.0, 1.0, 5.0))))
+        .addEqualityGroup(Mean.create(), Mean.create())
+        .addEqualityGroup(LastValue.create(), LastValue.create())
+        .testEquals();
+  }
+
+  @Test
+  public void testMatch() {
+    List<Aggregation> aggregations =
+        Arrays.asList(
+            Sum.create(),
+            Count.create(),
+            Mean.create(),
+            Distribution.create(BucketBoundaries.create(Arrays.asList(-10.0, 1.0, 5.0))),
+            LastValue.create());
+
+    List<String> actual = new ArrayList<String>();
+    for (Aggregation aggregation : aggregations) {
+      actual.add(
+          aggregation.match(
+              Functions.returnConstant("SUM"),
+              Functions.returnConstant("COUNT"),
+              Functions.returnConstant("DISTRIBUTION"),
+              Functions.returnConstant("LASTVALUE"),
+              Functions.returnConstant("UNKNOWN")));
+    }
+
+    assertThat(actual)
+        .isEqualTo(Arrays.asList("SUM", "COUNT", "UNKNOWN", "DISTRIBUTION", "LASTVALUE"));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/stats/BucketBoundariesTest.java b/api/src/test/java/io/opencensus/stats/BucketBoundariesTest.java
new file mode 100644
index 0000000..36f2edb
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/BucketBoundariesTest.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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link io.opencensus.stats.BucketBoundaries}. */
+@RunWith(JUnit4.class)
+public class BucketBoundariesTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testConstructBoundaries() {
+    List<Double> buckets = Arrays.asList(0.0, 1.0, 2.0);
+    BucketBoundaries bucketBoundaries = BucketBoundaries.create(buckets);
+    assertThat(bucketBoundaries.getBoundaries()).isEqualTo(buckets);
+  }
+
+  @Test
+  public void testBoundariesDoesNotChangeWithOriginalList() {
+    List<Double> original = new ArrayList<Double>();
+    original.add(0.0);
+    original.add(1.0);
+    original.add(2.0);
+    BucketBoundaries bucketBoundaries = BucketBoundaries.create(original);
+    original.set(2, 3.0);
+    original.add(4.0);
+    List<Double> expected = Arrays.asList(0.0, 1.0, 2.0);
+    assertThat(bucketBoundaries.getBoundaries()).isNotEqualTo(original);
+    assertThat(bucketBoundaries.getBoundaries()).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNullBoundaries() throws Exception {
+    thrown.expect(NullPointerException.class);
+    BucketBoundaries.create(null);
+  }
+
+  @Test
+  public void testUnsortedBoundaries() throws Exception {
+    List<Double> buckets = Arrays.asList(0.0, 1.0, 1.0);
+    thrown.expect(IllegalArgumentException.class);
+    BucketBoundaries.create(buckets);
+  }
+
+  @Test
+  public void testNoBoundaries() {
+    List<Double> buckets = Arrays.asList();
+    BucketBoundaries bucketBoundaries = BucketBoundaries.create(buckets);
+    assertThat(bucketBoundaries.getBoundaries()).isEqualTo(buckets);
+  }
+
+  @Test
+  public void testBucketBoundariesEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            BucketBoundaries.create(Arrays.asList(-1.0, 2.0)),
+            BucketBoundaries.create(Arrays.asList(-1.0, 2.0)))
+        .addEqualityGroup(BucketBoundaries.create(Arrays.asList(-1.0)))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/stats/MeasureTest.java b/api/src/test/java/io/opencensus/stats/MeasureTest.java
new file mode 100644
index 0000000..a930242
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/MeasureTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2016-17, 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Measure}. */
+@RunWith(JUnit4.class)
+public final class MeasureTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testConstants() {
+    assertThat(Measure.NAME_MAX_LENGTH).isEqualTo(255);
+  }
+
+  @Test
+  public void preventTooLongMeasureName() {
+    char[] chars = new char[Measure.NAME_MAX_LENGTH + 1];
+    Arrays.fill(chars, 'a');
+    String longName = String.valueOf(chars);
+    thrown.expect(IllegalArgumentException.class);
+    Measure.MeasureDouble.create(longName, "description", "1");
+  }
+
+  @Test
+  public void preventNonPrintableMeasureName() {
+    thrown.expect(IllegalArgumentException.class);
+    Measure.MeasureDouble.create("\2", "description", "1");
+  }
+
+  @Test
+  public void testMeasureDoubleComponents() {
+    Measure measurement = Measure.MeasureDouble.create("Foo", "The description of Foo", "Mbit/s");
+    assertThat(measurement.getName()).isEqualTo("Foo");
+    assertThat(measurement.getDescription()).isEqualTo("The description of Foo");
+    assertThat(measurement.getUnit()).isEqualTo("Mbit/s");
+  }
+
+  @Test
+  public void testMeasureLongComponents() {
+    Measure measurement = Measure.MeasureLong.create("Bar", "The description of Bar", "1");
+    assertThat(measurement.getName()).isEqualTo("Bar");
+    assertThat(measurement.getDescription()).isEqualTo("The description of Bar");
+    assertThat(measurement.getUnit()).isEqualTo("1");
+  }
+
+  @Test
+  public void testMeasureDoubleEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            Measure.MeasureDouble.create("name", "description", "bit/s"),
+            Measure.MeasureDouble.create("name", "description", "bit/s"))
+        .addEqualityGroup(Measure.MeasureDouble.create("name", "description 2", "bit/s"))
+        .testEquals();
+  }
+
+  @Test
+  public void testMeasureLongEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            Measure.MeasureLong.create("name", "description", "bit/s"),
+            Measure.MeasureLong.create("name", "description", "bit/s"))
+        .addEqualityGroup(Measure.MeasureLong.create("name", "description 2", "bit/s"))
+        .testEquals();
+  }
+
+  @Test
+  public void testMatch() {
+    List<Measure> measures =
+        Arrays.asList(
+            MeasureDouble.create("measure1", "description", "1"),
+            MeasureLong.create("measure2", "description", "1"));
+    List<String> outputs = Lists.newArrayList();
+    for (Measure measure : measures) {
+      outputs.add(
+          measure.match(
+              new Function<MeasureDouble, String>() {
+                @Override
+                public String apply(MeasureDouble arg) {
+                  return "double";
+                }
+              },
+              new Function<MeasureLong, String>() {
+                @Override
+                public String apply(MeasureLong arg) {
+                  return "long";
+                }
+              },
+              Functions.<String>throwAssertionError()));
+    }
+    assertThat(outputs).containsExactly("double", "long").inOrder();
+  }
+
+  @Test
+  public void testMeasureDoubleIsNotEqualToMeasureLong() {
+    assertThat(Measure.MeasureDouble.create("name", "description", "bit/s"))
+        .isNotEqualTo(Measure.MeasureLong.create("name", "description", "bit/s"));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/stats/NoopStatsTest.java b/api/src/test/java/io/opencensus/stats/NoopStatsTest.java
new file mode 100644
index 0000000..4bae14a
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/NoopStatsTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.Collections;
+import java.util.Iterator;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link NoopStats}. Tests for {@link NoopStats#newNoopViewManager} are in {@link
+ * NoopViewManagerTest}
+ */
+@RunWith(JUnit4.class)
+public final class NoopStatsTest {
+  private static final Tag TAG = Tag.create(TagKey.create("key"), TagValue.create("value"));
+  private static final MeasureDouble MEASURE =
+      Measure.MeasureDouble.create("my measure", "description", "s");
+
+  private final TagContext tagContext =
+      new TagContext() {
+
+        @Override
+        protected Iterator<Tag> getIterator() {
+          return Collections.<Tag>singleton(TAG).iterator();
+        }
+      };
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void noopStatsComponent() {
+    assertThat(NoopStats.newNoopStatsComponent().getStatsRecorder())
+        .isSameAs(NoopStats.getNoopStatsRecorder());
+    assertThat(NoopStats.newNoopStatsComponent().getViewManager())
+        .isInstanceOf(NoopStats.newNoopViewManager().getClass());
+  }
+
+  @Test
+  public void noopStatsComponent_GetState() {
+    assertThat(NoopStats.newNoopStatsComponent().getState())
+        .isEqualTo(StatsCollectionState.DISABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void noopStatsComponent_SetState_IgnoresInput() {
+    StatsComponent noopStatsComponent = NoopStats.newNoopStatsComponent();
+    noopStatsComponent.setState(StatsCollectionState.ENABLED);
+    assertThat(noopStatsComponent.getState()).isEqualTo(StatsCollectionState.DISABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void noopStatsComponent_SetState_DisallowsNull() {
+    StatsComponent noopStatsComponent = NoopStats.newNoopStatsComponent();
+    thrown.expect(NullPointerException.class);
+    noopStatsComponent.setState(null);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void noopStatsComponent_DisallowsSetStateAfterGetState() {
+    StatsComponent noopStatsComponent = NoopStats.newNoopStatsComponent();
+    noopStatsComponent.setState(StatsCollectionState.DISABLED);
+    noopStatsComponent.getState();
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("State was already read, cannot set state.");
+    noopStatsComponent.setState(StatsCollectionState.ENABLED);
+  }
+
+  @Test
+  public void noopStatsRecorder_PutAttachmentNullKey() {
+    MeasureMap measureMap = NoopStats.getNoopStatsRecorder().newMeasureMap();
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("key");
+    measureMap.putAttachment(null, "value");
+  }
+
+  @Test
+  public void noopStatsRecorder_PutAttachmentNullValue() {
+    MeasureMap measureMap = NoopStats.getNoopStatsRecorder().newMeasureMap();
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("value");
+    measureMap.putAttachment("key", null);
+  }
+
+  // The NoopStatsRecorder should do nothing, so this test just checks that record doesn't throw an
+  // exception.
+  @Test
+  public void noopStatsRecorder_Record() {
+    NoopStats.getNoopStatsRecorder().newMeasureMap().put(MEASURE, 5).record(tagContext);
+  }
+
+  // The NoopStatsRecorder should do nothing, so this test just checks that record doesn't throw an
+  // exception.
+  @Test
+  public void noopStatsRecorder_RecordWithCurrentContext() {
+    NoopStats.getNoopStatsRecorder().newMeasureMap().put(MEASURE, 6).record();
+  }
+
+  @Test
+  public void noopStatsRecorder_Record_DisallowNullTagContext() {
+    MeasureMap measureMap = NoopStats.getNoopStatsRecorder().newMeasureMap();
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("tags");
+    measureMap.record(null);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/stats/NoopViewManagerTest.java b/api/src/test/java/io/opencensus/stats/NoopViewManagerTest.java
new file mode 100644
index 0000000..44c7626
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/NoopViewManagerTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagKey;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link NoopStats#newNoopViewManager}. */
+@RunWith(JUnit4.class)
+public final class NoopViewManagerTest {
+  private static final MeasureDouble MEASURE =
+      Measure.MeasureDouble.create("my measure", "description", "s");
+  private static final TagKey KEY = TagKey.create("KEY");
+  private static final Name VIEW_NAME = Name.create("my view");
+  private static final String VIEW_DESCRIPTION = "view description";
+  private static final Sum AGGREGATION = Sum.create();
+  private static final Cumulative CUMULATIVE = Cumulative.create();
+  private static final Duration TEN_SECONDS = Duration.create(10, 0);
+  private static final Interval INTERVAL = Interval.create(TEN_SECONDS);
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void noopViewManager_RegisterView_DisallowRegisteringDifferentViewWithSameName() {
+    final View view1 =
+        View.create(
+            VIEW_NAME, "description 1", MEASURE, AGGREGATION, Arrays.asList(KEY), CUMULATIVE);
+    final View view2 =
+        View.create(
+            VIEW_NAME, "description 2", MEASURE, AGGREGATION, Arrays.asList(KEY), CUMULATIVE);
+    ViewManager viewManager = NoopStats.newNoopViewManager();
+    viewManager.registerView(view1);
+
+    try {
+      thrown.expect(IllegalArgumentException.class);
+      thrown.expectMessage("A different view with the same name already exists.");
+      viewManager.registerView(view2);
+    } finally {
+      assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(view1);
+    }
+  }
+
+  @Test
+  public void noopViewManager_RegisterView_AllowRegisteringSameViewTwice() {
+    View view =
+        View.create(
+            VIEW_NAME, VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY), CUMULATIVE);
+    ViewManager viewManager = NoopStats.newNoopViewManager();
+    viewManager.registerView(view);
+    viewManager.registerView(view);
+  }
+
+  @Test
+  public void noopViewManager_RegisterView_DisallowNull() {
+    ViewManager viewManager = NoopStats.newNoopViewManager();
+    thrown.expect(NullPointerException.class);
+    viewManager.registerView(null);
+  }
+
+  @Test
+  public void noopViewManager_GetView_GettingNonExistentViewReturnsNull() {
+    ViewManager viewManager = NoopStats.newNoopViewManager();
+    assertThat(viewManager.getView(VIEW_NAME)).isNull();
+  }
+
+  @Test
+  public void noopViewManager_GetView_Cumulative() {
+    View view =
+        View.create(
+            VIEW_NAME, VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY), CUMULATIVE);
+    ViewManager viewManager = NoopStats.newNoopViewManager();
+    viewManager.registerView(view);
+
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertThat(viewData.getView()).isEqualTo(view);
+    assertThat(viewData.getAggregationMap()).isEmpty();
+    assertThat(viewData.getStart()).isEqualTo(Timestamp.create(0, 0));
+    assertThat(viewData.getEnd()).isEqualTo(Timestamp.create(0, 0));
+    assertThat(viewData.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(0, 0), Timestamp.create(0, 0)));
+  }
+
+  @Test
+  public void noopViewManager_GetView_Interval() {
+    View view =
+        View.create(
+            VIEW_NAME, VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY), INTERVAL);
+    ViewManager viewManager = NoopStats.newNoopViewManager();
+    viewManager.registerView(view);
+
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertThat(viewData.getView()).isEqualTo(view);
+    assertThat(viewData.getAggregationMap()).isEmpty();
+    assertThat(viewData.getWindowData()).isEqualTo(IntervalData.create(Timestamp.create(0, 0)));
+  }
+
+  @Test
+  public void noopViewManager_GetView_DisallowNull() {
+    ViewManager viewManager = NoopStats.newNoopViewManager();
+    thrown.expect(NullPointerException.class);
+    viewManager.getView(null);
+  }
+
+  @Test
+  public void getAllExportedViews() {
+    ViewManager viewManager = NoopStats.newNoopViewManager();
+    assertThat(viewManager.getAllExportedViews()).isEmpty();
+    View cumulativeView1 =
+        View.create(
+            View.Name.create("View 1"),
+            VIEW_DESCRIPTION,
+            MEASURE,
+            AGGREGATION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    View cumulativeView2 =
+        View.create(
+            View.Name.create("View 2"),
+            VIEW_DESCRIPTION,
+            MEASURE,
+            AGGREGATION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    View intervalView =
+        View.create(
+            View.Name.create("View 3"),
+            VIEW_DESCRIPTION,
+            MEASURE,
+            AGGREGATION,
+            Arrays.asList(KEY),
+            INTERVAL);
+    viewManager.registerView(cumulativeView1);
+    viewManager.registerView(cumulativeView2);
+    viewManager.registerView(intervalView);
+
+    // Only cumulative views should be exported.
+    assertThat(viewManager.getAllExportedViews()).containsExactly(cumulativeView1, cumulativeView2);
+  }
+
+  @Test
+  public void getAllExportedViews_ResultIsUnmodifiable() {
+    ViewManager viewManager = NoopStats.newNoopViewManager();
+    View view1 =
+        View.create(
+            View.Name.create("View 1"), VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY));
+    viewManager.registerView(view1);
+    Set<View> exported = viewManager.getAllExportedViews();
+
+    View view2 =
+        View.create(
+            View.Name.create("View 2"), VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY));
+    thrown.expect(UnsupportedOperationException.class);
+    exported.add(view2);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/stats/StatsTest.java b/api/src/test/java/io/opencensus/stats/StatsTest.java
new file mode 100644
index 0000000..4219173
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/StatsTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2016-17, 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Stats}. */
+@RunWith(JUnit4.class)
+public final class StatsTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void loadStatsManager_UsesProvidedClassLoader() {
+    final RuntimeException toThrow = new RuntimeException("UseClassLoader");
+    thrown.expect(RuntimeException.class);
+    thrown.expectMessage("UseClassLoader");
+    Stats.loadStatsComponent(
+        new ClassLoader() {
+          @Override
+          public Class<?> loadClass(String name) {
+            throw toThrow;
+          }
+        });
+  }
+
+  @Test
+  public void loadStatsManager_IgnoresMissingClasses() {
+    ClassLoader classLoader =
+        new ClassLoader() {
+          @Override
+          public Class<?> loadClass(String name) throws ClassNotFoundException {
+            throw new ClassNotFoundException();
+          }
+        };
+
+    assertThat(Stats.loadStatsComponent(classLoader).getClass().getName())
+        .isEqualTo("io.opencensus.stats.NoopStats$NoopStatsComponent");
+  }
+
+  @Test
+  public void defaultValues() {
+    assertThat(Stats.getStatsRecorder()).isEqualTo(NoopStats.getNoopStatsRecorder());
+    assertThat(Stats.getViewManager()).isInstanceOf(NoopStats.newNoopViewManager().getClass());
+  }
+
+  @Test
+  public void getState() {
+    assertThat(Stats.getState()).isEqualTo(StatsCollectionState.DISABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void setState_IgnoresInput() {
+    Stats.setState(StatsCollectionState.ENABLED);
+    assertThat(Stats.getState()).isEqualTo(StatsCollectionState.DISABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void setState_DisallowsNull() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("state");
+    Stats.setState(null);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/stats/ViewDataTest.java b/api/src/test/java/io/opencensus/stats/ViewDataTest.java
new file mode 100644
index 0000000..0120ffe
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/ViewDataTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2016-17, 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.View.AggregationWindow;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.ViewData.AggregationWindowData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for class {@link ViewData}. */
+@RunWith(JUnit4.class)
+public final class ViewDataTest {
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testCumulativeViewData() {
+    View view = View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, CUMULATIVE);
+    Timestamp start = Timestamp.fromMillis(1000);
+    Timestamp end = Timestamp.fromMillis(2000);
+    AggregationWindowData windowData = CumulativeData.create(start, end);
+    ViewData viewData = ViewData.create(view, ENTRIES, windowData);
+    assertThat(viewData.getView()).isEqualTo(view);
+    assertThat(viewData.getAggregationMap()).isEqualTo(ENTRIES);
+    assertThat(viewData.getWindowData()).isEqualTo(windowData);
+  }
+
+  @Test
+  public void testIntervalViewData() {
+    View view =
+        View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, INTERVAL_HOUR);
+    Timestamp end = Timestamp.fromMillis(2000);
+    AggregationWindowData windowData = IntervalData.create(end);
+    ViewData viewData = ViewData.create(view, ENTRIES, windowData);
+    assertThat(viewData.getView()).isEqualTo(view);
+    assertThat(viewData.getAggregationMap()).isEqualTo(ENTRIES);
+    assertThat(viewData.getWindowData()).isEqualTo(windowData);
+  }
+
+  @Test
+  public void testViewDataEquals() {
+    View cumulativeView =
+        View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, CUMULATIVE);
+    View intervalView =
+        View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, INTERVAL_HOUR);
+
+    new EqualsTester()
+        .addEqualityGroup(
+            ViewData.create(
+                cumulativeView,
+                ENTRIES,
+                CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000))),
+            ViewData.create(
+                cumulativeView,
+                ENTRIES,
+                CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000))))
+        .addEqualityGroup(
+            ViewData.create(
+                cumulativeView,
+                ENTRIES,
+                CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(3000))))
+        .addEqualityGroup(
+            ViewData.create(intervalView, ENTRIES, IntervalData.create(Timestamp.fromMillis(2000))),
+            ViewData.create(intervalView, ENTRIES, IntervalData.create(Timestamp.fromMillis(2000))))
+        .addEqualityGroup(
+            ViewData.create(
+                intervalView,
+                Collections.<List<TagValue>, AggregationData>emptyMap(),
+                IntervalData.create(Timestamp.fromMillis(2000))))
+        .testEquals();
+  }
+
+  @Test
+  public void testAggregationWindowDataMatch() {
+    final Timestamp start = Timestamp.fromMillis(1000);
+    final Timestamp end = Timestamp.fromMillis(2000);
+    final AggregationWindowData windowData1 = CumulativeData.create(start, end);
+    final AggregationWindowData windowData2 = IntervalData.create(end);
+    windowData1.match(
+        new Function<CumulativeData, Void>() {
+          @Override
+          public Void apply(CumulativeData windowData) {
+            assertThat(windowData.getStart()).isEqualTo(start);
+            assertThat(windowData.getEnd()).isEqualTo(end);
+            return null;
+          }
+        },
+        new Function<IntervalData, Void>() {
+          @Override
+          public Void apply(IntervalData windowData) {
+            fail("CumulativeData expected.");
+            return null;
+          }
+        },
+        Functions.<Void>throwIllegalArgumentException());
+    windowData2.match(
+        new Function<CumulativeData, Void>() {
+          @Override
+          public Void apply(CumulativeData windowData) {
+            fail("IntervalData expected.");
+            return null;
+          }
+        },
+        new Function<IntervalData, Void>() {
+          @Override
+          public Void apply(IntervalData windowData) {
+            assertThat(windowData.getEnd()).isEqualTo(end);
+            return null;
+          }
+        },
+        Functions.<Void>throwIllegalArgumentException());
+  }
+
+  @Test
+  public void preventWindowAndAggregationWindowDataMismatch() {
+    CumulativeData cumulativeData =
+        CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage(
+        "AggregationWindow and AggregationWindowData types mismatch. "
+            + "AggregationWindow: "
+            + INTERVAL_HOUR.getClass().getSimpleName()
+            + " AggregationWindowData: "
+            + cumulativeData.getClass().getSimpleName());
+    ViewData.create(
+        View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, INTERVAL_HOUR),
+        ENTRIES,
+        cumulativeData);
+  }
+
+  @Test
+  public void preventWindowAndAggregationWindowDataMismatch2() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("AggregationWindow and AggregationWindowData types mismatch. ");
+    ViewData.create(
+        View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, CUMULATIVE),
+        ENTRIES,
+        IntervalData.create(Timestamp.fromMillis(1000)));
+  }
+
+  @Test
+  public void preventStartTimeLaterThanEndTime() {
+    thrown.expect(IllegalArgumentException.class);
+    CumulativeData.create(Timestamp.fromMillis(3000), Timestamp.fromMillis(2000));
+  }
+
+  @Test
+  public void preventAggregationAndAggregationDataMismatch_SumDouble_SumLong() {
+    aggregationAndAggregationDataMismatch(
+        createView(Sum.create(), MEASURE_DOUBLE),
+        ImmutableMap.<List<TagValue>, AggregationData>of(
+            Arrays.asList(V1, V2), SumDataLong.create(100)));
+  }
+
+  @Test
+  public void preventAggregationAndAggregationDataMismatch_SumLong_SumDouble() {
+    aggregationAndAggregationDataMismatch(
+        createView(Sum.create(), MEASURE_LONG),
+        ImmutableMap.<List<TagValue>, AggregationData>of(
+            Arrays.asList(V1, V2), SumDataDouble.create(100)));
+  }
+
+  @Test
+  public void preventAggregationAndAggregationDataMismatch_Count_Distribution() {
+    aggregationAndAggregationDataMismatch(createView(Count.create()), ENTRIES);
+  }
+
+  @Test
+  public void preventAggregationAndAggregationDataMismatch_Mean_Distribution() {
+    aggregationAndAggregationDataMismatch(createView(Mean.create()), ENTRIES);
+  }
+
+  @Test
+  public void preventAggregationAndAggregationDataMismatch_Distribution_Count() {
+    aggregationAndAggregationDataMismatch(
+        createView(DISTRIBUTION), ImmutableMap.of(Arrays.asList(V10, V20), CountData.create(100)));
+  }
+
+  @Test
+  public void preventAggregationAndAggregationDataMismatch_LastValueDouble_LastValueLong() {
+    aggregationAndAggregationDataMismatch(
+        createView(LastValue.create(), MEASURE_DOUBLE),
+        ImmutableMap.<List<TagValue>, AggregationData>of(
+            Arrays.asList(V1, V2), LastValueDataLong.create(100)));
+  }
+
+  @Test
+  public void preventAggregationAndAggregationDataMismatch_LastValueLong_LastValueDouble() {
+    aggregationAndAggregationDataMismatch(
+        createView(LastValue.create(), MEASURE_LONG),
+        ImmutableMap.<List<TagValue>, AggregationData>of(
+            Arrays.asList(V1, V2), LastValueDataDouble.create(100)));
+  }
+
+  private static View createView(Aggregation aggregation) {
+    return createView(aggregation, MEASURE_DOUBLE);
+  }
+
+  private static View createView(Aggregation aggregation, Measure measure) {
+    return View.create(NAME, DESCRIPTION, measure, aggregation, TAG_KEYS, CUMULATIVE);
+  }
+
+  private void aggregationAndAggregationDataMismatch(
+      View view, Map<List<TagValue>, ? extends AggregationData> entries) {
+    CumulativeData cumulativeData =
+        CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+    Aggregation aggregation = view.getAggregation();
+    AggregationData aggregationData = entries.values().iterator().next();
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage(
+        "Aggregation and AggregationData types mismatch. "
+            + "Aggregation: "
+            + aggregation.getClass().getSimpleName()
+            + " AggregationData: "
+            + aggregationData.getClass().getSimpleName());
+    ViewData.create(view, entries, cumulativeData);
+  }
+
+  // tag keys
+  private static final TagKey K1 = TagKey.create("k1");
+  private static final TagKey K2 = TagKey.create("k2");
+  private static final List<TagKey> TAG_KEYS = Arrays.asList(K1, K2);
+
+  // tag values
+  private static final TagValue V1 = TagValue.create("v1");
+  private static final TagValue V2 = TagValue.create("v2");
+  private static final TagValue V10 = TagValue.create("v10");
+  private static final TagValue V20 = TagValue.create("v20");
+
+  private static final AggregationWindow CUMULATIVE = Cumulative.create();
+  private static final AggregationWindow INTERVAL_HOUR = Interval.create(Duration.create(3600, 0));
+
+  private static final BucketBoundaries BUCKET_BOUNDARIES =
+      BucketBoundaries.create(Arrays.asList(10.0, 20.0, 30.0, 40.0));
+
+  private static final Aggregation DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+
+  private static final ImmutableMap<List<TagValue>, DistributionData> ENTRIES =
+      ImmutableMap.of(
+          Arrays.asList(V1, V2),
+          DistributionData.create(1, 1, 1, 1, 0, Arrays.asList(0L, 1L, 0L)),
+          Arrays.asList(V10, V20),
+          DistributionData.create(-5, 6, -20, 5, 100.1, Arrays.asList(5L, 0L, 1L)));
+
+  // name
+  private static final View.Name NAME = View.Name.create("test-view");
+  // description
+  private static final String DESCRIPTION = "test-view-descriptor description";
+  // measure
+  private static final Measure MEASURE_DOUBLE =
+      Measure.MeasureDouble.create("measure1", "measure description", "1");
+  private static final Measure MEASURE_LONG =
+      Measure.MeasureLong.create("measure2", "measure description", "1");
+}
diff --git a/api/src/test/java/io/opencensus/stats/ViewTest.java b/api/src/test/java/io/opencensus/stats/ViewTest.java
new file mode 100644
index 0000000..afba1bc
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/ViewTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2016-17, 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.tags.TagKey;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link View}. */
+@RunWith(JUnit4.class)
+public final class ViewTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testConstants() {
+    assertThat(View.NAME_MAX_LENGTH).isEqualTo(255);
+  }
+
+  @Test
+  public void sortTagKeys() {
+    final View view =
+        View.create(
+            NAME,
+            DESCRIPTION,
+            MEASURE,
+            MEAN,
+            Arrays.asList(
+                TagKey.create("ab"), TagKey.create("a"), TagKey.create("A"), TagKey.create("b")));
+    assertThat(view.getColumns())
+        .containsExactly(
+            TagKey.create("A"), TagKey.create("a"), TagKey.create("ab"), TagKey.create("b"))
+        .inOrder();
+  }
+
+  @Test
+  public void testDistributionView() {
+    final View view = View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS);
+    assertThat(view.getName()).isEqualTo(NAME);
+    assertThat(view.getDescription()).isEqualTo(DESCRIPTION);
+    assertThat(view.getMeasure().getName()).isEqualTo(MEASURE.getName());
+    assertThat(view.getAggregation()).isEqualTo(MEAN);
+    assertThat(view.getColumns()).containsExactly(BAR, FOO).inOrder();
+    assertThat(view.getWindow()).isEqualTo(Cumulative.create());
+  }
+
+  @Test
+  public void testIntervalView() {
+    final View view = View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Interval.create(MINUTE));
+    assertThat(view.getName()).isEqualTo(NAME);
+    assertThat(view.getDescription()).isEqualTo(DESCRIPTION);
+    assertThat(view.getMeasure().getName()).isEqualTo(MEASURE.getName());
+    assertThat(view.getAggregation()).isEqualTo(MEAN);
+    assertThat(view.getColumns()).containsExactly(BAR, FOO).inOrder();
+    assertThat(view.getWindow()).isEqualTo(Interval.create(MINUTE));
+  }
+
+  @Test
+  public void testViewEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS),
+            View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Cumulative.create()))
+        .addEqualityGroup(
+            View.create(NAME, DESCRIPTION + 2, MEASURE, MEAN, KEYS, Cumulative.create()))
+        .addEqualityGroup(
+            View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Interval.create(MINUTE)),
+            View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Interval.create(MINUTE)))
+        .addEqualityGroup(
+            View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Interval.create(TWO_MINUTES)))
+        .testEquals();
+  }
+
+  @Test
+  public void preventDuplicateColumns() {
+    TagKey key1 = TagKey.create("duplicate");
+    TagKey key2 = TagKey.create("duplicate");
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Columns have duplicate.");
+    View.create(NAME, DESCRIPTION, MEASURE, MEAN, Arrays.asList(key1, key2));
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void preventNullViewName() {
+    View.create(null, DESCRIPTION, MEASURE, MEAN, KEYS);
+  }
+
+  @Test
+  public void preventTooLongViewName() {
+    char[] chars = new char[View.NAME_MAX_LENGTH + 1];
+    Arrays.fill(chars, 'a');
+    String longName = String.valueOf(chars);
+    thrown.expect(IllegalArgumentException.class);
+    View.Name.create(longName);
+  }
+
+  @Test
+  public void preventNonPrintableViewName() {
+    thrown.expect(IllegalArgumentException.class);
+    View.Name.create("\2");
+  }
+
+  @Test
+  public void testViewName() {
+    assertThat(View.Name.create("my name").asString()).isEqualTo("my name");
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void preventNullNameString() {
+    View.Name.create(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void preventNegativeIntervalDuration() {
+    Interval.create(NEG_TEN_SECONDS);
+  }
+
+  @Test
+  public void testViewNameEquals() {
+    new EqualsTester()
+        .addEqualityGroup(View.Name.create("view-1"), View.Name.create("view-1"))
+        .addEqualityGroup(View.Name.create("view-2"))
+        .testEquals();
+  }
+
+  private static final View.Name NAME = View.Name.create("test-view-name");
+  private static final String DESCRIPTION = "test-view-name description";
+  private static final Measure MEASURE =
+      Measure.MeasureDouble.create("measure", "measure description", "1");
+  private static final TagKey FOO = TagKey.create("foo");
+  private static final TagKey BAR = TagKey.create("bar");
+  private static final List<TagKey> KEYS = Collections.unmodifiableList(Arrays.asList(FOO, BAR));
+  private static final Mean MEAN = Mean.create();
+  private static final Duration MINUTE = Duration.create(60, 0);
+  private static final Duration TWO_MINUTES = Duration.create(120, 0);
+  private static final Duration NEG_TEN_SECONDS = Duration.create(-10, 0);
+}
diff --git a/api/src/test/java/io/opencensus/tags/InternalUtilsTest.java b/api/src/test/java/io/opencensus/tags/InternalUtilsTest.java
new file mode 100644
index 0000000..65482de
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/InternalUtilsTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import java.util.Iterator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InternalUtils}. */
+@RunWith(JUnit4.class)
+public final class InternalUtilsTest {
+
+  @Test
+  public void getTags() {
+    final Iterator<Tag> iterator =
+        Lists.<Tag>newArrayList(Tag.create(TagKey.create("k"), TagValue.create("v"))).iterator();
+    TagContext ctx =
+        new TagContext() {
+          @Override
+          protected Iterator<Tag> getIterator() {
+            return iterator;
+          }
+        };
+    assertThat(InternalUtils.getTags(ctx)).isSameAs(iterator);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/tags/NoopTagsTest.java b/api/src/test/java/io/opencensus/tags/NoopTagsTest.java
new file mode 100644
index 0000000..db07520
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/NoopTagsTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.opencensus.internal.NoopScope;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import io.opencensus.tags.propagation.TagContextSerializationException;
+import java.util.Arrays;
+import java.util.Iterator;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link NoopTags}. */
+@RunWith(JUnit4.class)
+public final class NoopTagsTest {
+  private static final TagKey KEY = TagKey.create("key");
+  private static final TagValue VALUE = TagValue.create("value");
+
+  private static final TagContext TAG_CONTEXT =
+      new TagContext() {
+
+        @Override
+        protected Iterator<Tag> getIterator() {
+          return Arrays.<Tag>asList(Tag.create(KEY, VALUE)).iterator();
+        }
+      };
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void noopTagsComponent() {
+    assertThat(NoopTags.newNoopTagsComponent().getTagger()).isSameAs(NoopTags.getNoopTagger());
+    assertThat(NoopTags.newNoopTagsComponent().getTagPropagationComponent())
+        .isSameAs(NoopTags.getNoopTagPropagationComponent());
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void noopTagsComponent_SetState_DisallowsNull() {
+    TagsComponent noopTagsComponent = NoopTags.newNoopTagsComponent();
+    thrown.expect(NullPointerException.class);
+    noopTagsComponent.setState(null);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void preventSettingStateAfterGettingState_DifferentState() {
+    TagsComponent noopTagsComponent = NoopTags.newNoopTagsComponent();
+    noopTagsComponent.setState(TaggingState.DISABLED);
+    noopTagsComponent.getState();
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("State was already read, cannot set state.");
+    noopTagsComponent.setState(TaggingState.ENABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void preventSettingStateAfterGettingState_SameState() {
+    TagsComponent noopTagsComponent = NoopTags.newNoopTagsComponent();
+    noopTagsComponent.setState(TaggingState.DISABLED);
+    noopTagsComponent.getState();
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("State was already read, cannot set state.");
+    noopTagsComponent.setState(TaggingState.DISABLED);
+  }
+
+  @Test
+  public void noopTagger() {
+    Tagger noopTagger = NoopTags.getNoopTagger();
+    assertThat(noopTagger.empty()).isSameAs(NoopTags.getNoopTagContext());
+    assertThat(noopTagger.getCurrentTagContext()).isSameAs(NoopTags.getNoopTagContext());
+    assertThat(noopTagger.emptyBuilder()).isSameAs(NoopTags.getNoopTagContextBuilder());
+    assertThat(noopTagger.toBuilder(TAG_CONTEXT)).isSameAs(NoopTags.getNoopTagContextBuilder());
+    assertThat(noopTagger.currentBuilder()).isSameAs(NoopTags.getNoopTagContextBuilder());
+    assertThat(noopTagger.withTagContext(TAG_CONTEXT)).isSameAs(NoopScope.getInstance());
+  }
+
+  @Test
+  public void noopTagger_ToBuilder_DisallowsNull() {
+    Tagger noopTagger = NoopTags.getNoopTagger();
+    thrown.expect(NullPointerException.class);
+    noopTagger.toBuilder(null);
+  }
+
+  @Test
+  public void noopTagger_WithTagContext_DisallowsNull() {
+    Tagger noopTagger = NoopTags.getNoopTagger();
+    thrown.expect(NullPointerException.class);
+    noopTagger.withTagContext(null);
+  }
+
+  @Test
+  public void noopTagContextBuilder() {
+    assertThat(NoopTags.getNoopTagContextBuilder().build()).isSameAs(NoopTags.getNoopTagContext());
+    assertThat(NoopTags.getNoopTagContextBuilder().put(KEY, VALUE).build())
+        .isSameAs(NoopTags.getNoopTagContext());
+    assertThat(NoopTags.getNoopTagContextBuilder().buildScoped()).isSameAs(NoopScope.getInstance());
+    assertThat(NoopTags.getNoopTagContextBuilder().put(KEY, VALUE).buildScoped())
+        .isSameAs(NoopScope.getInstance());
+  }
+
+  @Test
+  public void noopTagContextBuilder_Put_DisallowsNullKey() {
+    TagContextBuilder noopBuilder = NoopTags.getNoopTagContextBuilder();
+    thrown.expect(NullPointerException.class);
+    noopBuilder.put(null, VALUE);
+  }
+
+  @Test
+  public void noopTagContextBuilder_Put_DisallowsNullValue() {
+    TagContextBuilder noopBuilder = NoopTags.getNoopTagContextBuilder();
+    thrown.expect(NullPointerException.class);
+    noopBuilder.put(KEY, null);
+  }
+
+  @Test
+  public void noopTagContextBuilder_Remove_DisallowsNullKey() {
+    TagContextBuilder noopBuilder = NoopTags.getNoopTagContextBuilder();
+    thrown.expect(NullPointerException.class);
+    noopBuilder.remove(null);
+  }
+
+  @Test
+  public void noopTagContext() {
+    assertThat(Lists.newArrayList(NoopTags.getNoopTagContext().getIterator())).isEmpty();
+  }
+
+  @Test
+  public void noopTagPropagationComponent() {
+    assertThat(NoopTags.getNoopTagPropagationComponent().getBinarySerializer())
+        .isSameAs(NoopTags.getNoopTagContextBinarySerializer());
+  }
+
+  @Test
+  public void noopTagContextBinarySerializer()
+      throws TagContextDeserializationException, TagContextSerializationException {
+    assertThat(NoopTags.getNoopTagContextBinarySerializer().toByteArray(TAG_CONTEXT))
+        .isEqualTo(new byte[0]);
+    assertThat(NoopTags.getNoopTagContextBinarySerializer().fromByteArray(new byte[5]))
+        .isEqualTo(NoopTags.getNoopTagContext());
+  }
+
+  @Test
+  public void noopTagContextBinarySerializer_ToByteArray_DisallowsNull()
+      throws TagContextSerializationException {
+    TagContextBinarySerializer noopSerializer = NoopTags.getNoopTagContextBinarySerializer();
+    thrown.expect(NullPointerException.class);
+    noopSerializer.toByteArray(null);
+  }
+
+  @Test
+  public void noopTagContextBinarySerializer_FromByteArray_DisallowsNull()
+      throws TagContextDeserializationException {
+    TagContextBinarySerializer noopSerializer = NoopTags.getNoopTagContextBinarySerializer();
+    thrown.expect(NullPointerException.class);
+    noopSerializer.fromByteArray(null);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagContextTest.java b/api/src/test/java/io/opencensus/tags/TagContextTest.java
new file mode 100644
index 0000000..025c7ba
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagContextTest.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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TagContext}. */
+@RunWith(JUnit4.class)
+public final class TagContextTest {
+  private static final Tag TAG1 = Tag.create(TagKey.create("key"), TagValue.create("val"));
+  private static final Tag TAG2 = Tag.create(TagKey.create("key2"), TagValue.create("val"));
+
+  @Test
+  public void equals_IgnoresTagOrderAndTagContextClass() {
+    new EqualsTester()
+        .addEqualityGroup(
+            new SimpleTagContext(TAG1, TAG2),
+            new SimpleTagContext(TAG1, TAG2),
+            new SimpleTagContext(TAG2, TAG1),
+            new TagContext() {
+              @Override
+              protected Iterator<Tag> getIterator() {
+                return Lists.newArrayList(TAG1, TAG2).iterator();
+              }
+            })
+        .testEquals();
+  }
+
+  @Test
+  public void equals_HandlesNullIterator() {
+    new EqualsTester()
+        .addEqualityGroup(
+            new SimpleTagContext((List<Tag>) null),
+            new SimpleTagContext((List<Tag>) null),
+            new SimpleTagContext())
+        .testEquals();
+  }
+
+  @Test
+  public void equals_DoesNotIgnoreNullTags() {
+    new EqualsTester()
+        .addEqualityGroup(new SimpleTagContext(TAG1))
+        .addEqualityGroup(new SimpleTagContext(TAG1, null), new SimpleTagContext(null, TAG1))
+        .addEqualityGroup(new SimpleTagContext(TAG1, null, null))
+        .testEquals();
+  }
+
+  @Test
+  public void equals_DoesNotIgnoreDuplicateTags() {
+    new EqualsTester()
+        .addEqualityGroup(new SimpleTagContext(TAG1))
+        .addEqualityGroup(new SimpleTagContext(TAG1, TAG1))
+        .testEquals();
+  }
+
+  @Test
+  public void testToString() {
+    assertThat(new SimpleTagContext().toString()).isEqualTo("TagContext");
+    assertThat(new SimpleTagContext(TAG1, TAG2).toString()).isEqualTo("TagContext");
+  }
+
+  private static final class SimpleTagContext extends TagContext {
+    @Nullable private final List<Tag> tags;
+
+    SimpleTagContext(Tag... tags) {
+      this(Lists.newArrayList(tags));
+    }
+
+    SimpleTagContext(List<Tag> tags) {
+      this.tags = tags == null ? null : Collections.unmodifiableList(Lists.newArrayList(tags));
+    }
+
+    @Override
+    @Nullable
+    protected Iterator<Tag> getIterator() {
+      return tags == null ? null : tags.iterator();
+    }
+  }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagKeyTest.java b/api/src/test/java/io/opencensus/tags/TagKeyTest.java
new file mode 100644
index 0000000..48cf9fd
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagKeyTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TagKey}. */
+@RunWith(JUnit4.class)
+public final class TagKeyTest {
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testMaxLength() {
+    assertThat(TagKey.MAX_LENGTH).isEqualTo(255);
+  }
+
+  @Test
+  public void testGetName() {
+    assertThat(TagKey.create("foo").getName()).isEqualTo("foo");
+  }
+
+  @Test
+  public void create_AllowTagKeyNameWithMaxLength() {
+    char[] chars = new char[TagKey.MAX_LENGTH];
+    Arrays.fill(chars, 'k');
+    String key = new String(chars);
+    assertThat(TagKey.create(key).getName()).isEqualTo(key);
+  }
+
+  @Test
+  public void create_DisallowTagKeyNameOverMaxLength() {
+    char[] chars = new char[TagKey.MAX_LENGTH + 1];
+    Arrays.fill(chars, 'k');
+    String key = new String(chars);
+    thrown.expect(IllegalArgumentException.class);
+    TagKey.create(key);
+  }
+
+  @Test
+  public void create_DisallowUnprintableChars() {
+    thrown.expect(IllegalArgumentException.class);
+    TagKey.create("\2ab\3cd");
+  }
+
+  @Test
+  public void createString_DisallowEmpty() {
+    thrown.expect(IllegalArgumentException.class);
+    TagKey.create("");
+  }
+
+  @Test
+  public void testTagKeyEquals() {
+    new EqualsTester()
+        .addEqualityGroup(TagKey.create("foo"), TagKey.create("foo"))
+        .addEqualityGroup(TagKey.create("bar"))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagTest.java b/api/src/test/java/io/opencensus/tags/TagTest.java
new file mode 100644
index 0000000..3c899e6
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Tag}. */
+@RunWith(JUnit4.class)
+public final class TagTest {
+
+  @Test
+  public void testGetKey() {
+    assertThat(Tag.create(TagKey.create("k"), TagValue.create("v")).getKey())
+        .isEqualTo(TagKey.create("k"));
+  }
+
+  @Test
+  public void testTagEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            Tag.create(TagKey.create("Key"), TagValue.create("foo")),
+            Tag.create(TagKey.create("Key"), TagValue.create("foo")))
+        .addEqualityGroup(Tag.create(TagKey.create("Key"), TagValue.create("bar")))
+        .addEqualityGroup(Tag.create(TagKey.create("Key2"), TagValue.create("foo")))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagValueTest.java b/api/src/test/java/io/opencensus/tags/TagValueTest.java
new file mode 100644
index 0000000..9aa42c8
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagValueTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TagValue}. */
+@RunWith(JUnit4.class)
+public final class TagValueTest {
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testMaxLength() {
+    assertThat(TagValue.MAX_LENGTH).isEqualTo(255);
+  }
+
+  @Test
+  public void testAsString() {
+    assertThat(TagValue.create("foo").asString()).isEqualTo("foo");
+  }
+
+  @Test
+  public void create_AllowTagValueWithMaxLength() {
+    char[] chars = new char[TagValue.MAX_LENGTH];
+    Arrays.fill(chars, 'v');
+    String value = new String(chars);
+    assertThat(TagValue.create(value).asString()).isEqualTo(value);
+  }
+
+  @Test
+  public void create_DisallowTagValueOverMaxLength() {
+    char[] chars = new char[TagValue.MAX_LENGTH + 1];
+    Arrays.fill(chars, 'v');
+    String value = new String(chars);
+    thrown.expect(IllegalArgumentException.class);
+    TagValue.create(value);
+  }
+
+  @Test
+  public void disallowTagValueWithUnprintableChars() {
+    String value = "\2ab\3cd";
+    thrown.expect(IllegalArgumentException.class);
+    TagValue.create(value);
+  }
+
+  @Test
+  public void testTagValueEquals() {
+    new EqualsTester()
+        .addEqualityGroup(TagValue.create("foo"), TagValue.create("foo"))
+        .addEqualityGroup(TagValue.create("bar"))
+        .testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagsTest.java b/api/src/test/java/io/opencensus/tags/TagsTest.java
new file mode 100644
index 0000000..dee517b
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagsTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Tags}. */
+@RunWith(JUnit4.class)
+public class TagsTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void loadTagsComponent_UsesProvidedClassLoader() {
+    final RuntimeException toThrow = new RuntimeException("UseClassLoader");
+    thrown.expect(RuntimeException.class);
+    thrown.expectMessage("UseClassLoader");
+    Tags.loadTagsComponent(
+        new ClassLoader() {
+          @Override
+          public Class<?> loadClass(String name) {
+            throw toThrow;
+          }
+        });
+  }
+
+  @Test
+  public void loadTagsComponent_IgnoresMissingClasses() {
+    ClassLoader classLoader =
+        new ClassLoader() {
+          @Override
+          public Class<?> loadClass(String name) throws ClassNotFoundException {
+            throw new ClassNotFoundException();
+          }
+        };
+    assertThat(Tags.loadTagsComponent(classLoader).getClass().getName())
+        .isEqualTo("io.opencensus.tags.NoopTags$NoopTagsComponent");
+  }
+
+  // There is only one test that modifies tagging state in the Tags class, since the state is
+  // global, and it could affect other tests. NoopTagsTest has more thorough tests for tagging
+  // state.
+  @Test
+  @SuppressWarnings("deprecation")
+  public void testState() {
+    // Test that setState ignores its input.
+    Tags.setState(TaggingState.ENABLED);
+    assertThat(Tags.getState()).isEqualTo(TaggingState.DISABLED);
+
+    // Test that setState cannot be called after getState.
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("State was already read, cannot set state.");
+    Tags.setState(TaggingState.ENABLED);
+  }
+
+  @Test
+  public void defaultTagger() {
+    assertThat(Tags.getTagger()).isEqualTo(NoopTags.getNoopTagger());
+  }
+
+  @Test
+  public void defaultTagContextSerializer() {
+    assertThat(Tags.getTagPropagationComponent())
+        .isEqualTo(NoopTags.getNoopTagPropagationComponent());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/tags/propagation/TagContextDeserializationExceptionTest.java b/api/src/test/java/io/opencensus/tags/propagation/TagContextDeserializationExceptionTest.java
new file mode 100644
index 0000000..750d5d4
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/propagation/TagContextDeserializationExceptionTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TagContextDeserializationException}. */
+@RunWith(JUnit4.class)
+public final class TagContextDeserializationExceptionTest {
+
+  @Test
+  public void createWithMessage() {
+    assertThat(new TagContextDeserializationException("my message").getMessage())
+        .isEqualTo("my message");
+  }
+
+  @Test
+  public void createWithMessageAndCause() {
+    IOException cause = new IOException();
+    TagContextDeserializationException exception =
+        new TagContextDeserializationException("my message", cause);
+    assertThat(exception.getMessage()).isEqualTo("my message");
+    assertThat(exception.getCause()).isEqualTo(cause);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/tags/propagation/TagContextSerializationExceptionTest.java b/api/src/test/java/io/opencensus/tags/propagation/TagContextSerializationExceptionTest.java
new file mode 100644
index 0000000..54e9fab
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/propagation/TagContextSerializationExceptionTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TagContextSerializationException}. */
+@RunWith(JUnit4.class)
+public final class TagContextSerializationExceptionTest {
+
+  @Test
+  public void createWithMessage() {
+    assertThat(new TagContextSerializationException("my message").getMessage())
+        .isEqualTo("my message");
+  }
+
+  @Test
+  public void createWithMessageAndCause() {
+    IOException cause = new IOException();
+    TagContextSerializationException exception =
+        new TagContextSerializationException("my message", cause);
+    assertThat(exception.getMessage()).isEqualTo("my message");
+    assertThat(exception.getCause()).isEqualTo(cause);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/tags/unsafe/ContextUtilsTest.java b/api/src/test/java/io/opencensus/tags/unsafe/ContextUtilsTest.java
new file mode 100644
index 0000000..c35c5dc
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/unsafe/ContextUtilsTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.tags.unsafe;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.grpc.Context;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ContextUtils}. */
+@RunWith(JUnit4.class)
+public final class ContextUtilsTest {
+  @Test
+  public void testContextKeyName() {
+    // Context.Key.toString() returns the name.
+    assertThat(ContextUtils.TAG_CONTEXT_KEY.toString()).isEqualTo("opencensus-tag-context-key");
+  }
+
+  @Test
+  public void testGetCurrentTagContext_DefaultContext() {
+    TagContext tags = ContextUtils.TAG_CONTEXT_KEY.get();
+    assertThat(tags).isNotNull();
+    assertThat(asList(tags)).isEmpty();
+  }
+
+  @Test
+  public void testGetCurrentTagContext_ContextSetToNull() {
+    Context orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, null).attach();
+    try {
+      TagContext tags = ContextUtils.TAG_CONTEXT_KEY.get();
+      assertThat(tags).isNotNull();
+      assertThat(asList(tags)).isEmpty();
+    } finally {
+      Context.current().detach(orig);
+    }
+  }
+
+  private static List<Tag> asList(TagContext tags) {
+    return Lists.newArrayList(InternalUtils.getTags(tags));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/AnnotationTest.java b/api/src/test/java/io/opencensus/trace/AnnotationTest.java
new file mode 100644
index 0000000..0db5d93
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/AnnotationTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Link}. */
+@RunWith(JUnit4.class)
+public class AnnotationTest {
+  @Test(expected = NullPointerException.class)
+  public void fromDescription_NullDescription() {
+    Annotation.fromDescription(null);
+  }
+
+  @Test
+  public void fromDescription() {
+    Annotation annotation = Annotation.fromDescription("MyAnnotationText");
+    assertThat(annotation.getDescription()).isEqualTo("MyAnnotationText");
+    assertThat(annotation.getAttributes().size()).isEqualTo(0);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void fromDescriptionAndAttributes_NullDescription() {
+    Annotation.fromDescriptionAndAttributes(null, Collections.<String, AttributeValue>emptyMap());
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void fromDescriptionAndAttributes_NullAttributes() {
+    Annotation.fromDescriptionAndAttributes("", null);
+  }
+
+  @Test
+  public void fromDescriptionAndAttributes() {
+    Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+    attributes.put(
+        "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+    Annotation annotation = Annotation.fromDescriptionAndAttributes("MyAnnotationText", attributes);
+    assertThat(annotation.getDescription()).isEqualTo("MyAnnotationText");
+    assertThat(annotation.getAttributes()).isEqualTo(attributes);
+  }
+
+  @Test
+  public void fromDescriptionAndAttributes_EmptyAttributes() {
+    Annotation annotation =
+        Annotation.fromDescriptionAndAttributes(
+            "MyAnnotationText", Collections.<String, AttributeValue>emptyMap());
+    assertThat(annotation.getDescription()).isEqualTo("MyAnnotationText");
+    assertThat(annotation.getAttributes().size()).isEqualTo(0);
+  }
+
+  @Test
+  public void annotation_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+    attributes.put(
+        "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+    tester
+        .addEqualityGroup(
+            Annotation.fromDescription("MyAnnotationText"),
+            Annotation.fromDescriptionAndAttributes(
+                "MyAnnotationText", Collections.<String, AttributeValue>emptyMap()))
+        .addEqualityGroup(
+            Annotation.fromDescriptionAndAttributes("MyAnnotationText", attributes),
+            Annotation.fromDescriptionAndAttributes("MyAnnotationText", attributes))
+        .addEqualityGroup(Annotation.fromDescription("MyAnnotationText2"));
+    tester.testEquals();
+  }
+
+  @Test
+  public void annotation_ToString() {
+    Annotation annotation = Annotation.fromDescription("MyAnnotationText");
+    assertThat(annotation.toString()).contains("MyAnnotationText");
+    Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+    attributes.put(
+        "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+    annotation = Annotation.fromDescriptionAndAttributes("MyAnnotationText2", attributes);
+    assertThat(annotation.toString()).contains("MyAnnotationText2");
+    assertThat(annotation.toString()).contains(attributes.toString());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/AttributeValueTest.java b/api/src/test/java/io/opencensus/trace/AttributeValueTest.java
new file mode 100644
index 0000000..05ef43c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/AttributeValueTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AttributeValue}. */
+@RunWith(JUnit4.class)
+public class AttributeValueTest {
+  @Test
+  public void stringAttributeValue() {
+    AttributeValue attribute = AttributeValue.stringAttributeValue("MyStringAttributeValue");
+    attribute.match(
+        new Function<String, Object>() {
+          @Override
+          @Nullable
+          public Object apply(String stringValue) {
+            assertThat(stringValue).isEqualTo("MyStringAttributeValue");
+            return null;
+          }
+        },
+        new Function<Boolean, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Boolean booleanValue) {
+            fail("Expected a String");
+            return null;
+          }
+        },
+        new Function<Long, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Long longValue) {
+            fail("Expected a String");
+            return null;
+          }
+        },
+        Functions.throwIllegalArgumentException());
+  }
+
+  @Test
+  public void booleanAttributeValue() {
+    AttributeValue attribute = AttributeValue.booleanAttributeValue(true);
+    attribute.match(
+        new Function<String, Object>() {
+          @Override
+          @Nullable
+          public Object apply(String stringValue) {
+            fail("Expected a Boolean");
+            return null;
+          }
+        },
+        new Function<Boolean, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Boolean booleanValue) {
+            assertThat(booleanValue).isTrue();
+            return null;
+          }
+        },
+        new Function<Long, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Long longValue) {
+            fail("Expected a Boolean");
+            return null;
+          }
+        },
+        Functions.throwIllegalArgumentException());
+  }
+
+  @Test
+  public void longAttributeValue() {
+    AttributeValue attribute = AttributeValue.longAttributeValue(123456L);
+    attribute.match(
+        new Function<String, Object>() {
+          @Override
+          @Nullable
+          public Object apply(String stringValue) {
+            fail("Expected a Long");
+            return null;
+          }
+        },
+        new Function<Boolean, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Boolean booleanValue) {
+            fail("Expected a Long");
+            return null;
+          }
+        },
+        new Function<Long, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Long longValue) {
+            assertThat(longValue).isEqualTo(123456L);
+            return null;
+          }
+        },
+        Functions.throwIllegalArgumentException());
+  }
+
+  @Test
+  public void doubleAttributeValue() {
+    AttributeValue attribute = AttributeValue.doubleAttributeValue(1.23456);
+    attribute.match(
+        new Function<String, Object>() {
+          @Override
+          @Nullable
+          public Object apply(String stringValue) {
+            fail("Expected a Double");
+            return null;
+          }
+        },
+        new Function<Boolean, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Boolean booleanValue) {
+            fail("Expected a Double");
+            return null;
+          }
+        },
+        new Function<Long, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Long longValue) {
+            fail("Expected a Double");
+            return null;
+          }
+        },
+        new Function<Double, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Double doubleValue) {
+            assertThat(doubleValue).isEqualTo(1.23456);
+            return null;
+          }
+        },
+        Functions.throwIllegalArgumentException());
+  }
+
+  @Test
+  public void doubleAttributeValue_DeprecatedMatchFunction() {
+    AttributeValue attribute = AttributeValue.doubleAttributeValue(1.23456);
+    attribute.match(
+        new Function<String, Object>() {
+          @Override
+          @Nullable
+          public Object apply(String stringValue) {
+            fail("Expected a Double");
+            return null;
+          }
+        },
+        new Function<Boolean, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Boolean booleanValue) {
+            fail("Expected a Double");
+            return null;
+          }
+        },
+        new Function<Long, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Long longValue) {
+            fail("Expected a Double");
+            return null;
+          }
+        },
+        new Function<Object, Object>() {
+          @Override
+          @Nullable
+          public Object apply(Object value) {
+            assertThat(value).isEqualTo(1.23456);
+            return null;
+          }
+        });
+  }
+
+  @Test
+  public void attributeValue_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    tester.addEqualityGroup(
+        AttributeValue.stringAttributeValue("MyStringAttributeValue"),
+        AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+    tester.addEqualityGroup(AttributeValue.stringAttributeValue("MyStringAttributeDiffValue"));
+    tester.addEqualityGroup(
+        AttributeValue.booleanAttributeValue(true), AttributeValue.booleanAttributeValue(true));
+    tester.addEqualityGroup(AttributeValue.booleanAttributeValue(false));
+    tester.addEqualityGroup(
+        AttributeValue.longAttributeValue(123456L), AttributeValue.longAttributeValue(123456L));
+    tester.addEqualityGroup(AttributeValue.longAttributeValue(1234567L));
+    tester.addEqualityGroup(
+        AttributeValue.doubleAttributeValue(1.23456), AttributeValue.doubleAttributeValue(1.23456));
+    tester.addEqualityGroup(AttributeValue.doubleAttributeValue(1.234567));
+    tester.testEquals();
+  }
+
+  @Test
+  public void attributeValue_ToString() {
+    AttributeValue attribute = AttributeValue.stringAttributeValue("MyStringAttributeValue");
+    assertThat(attribute.toString()).contains("MyStringAttributeValue");
+    attribute = AttributeValue.booleanAttributeValue(true);
+    assertThat(attribute.toString()).contains("true");
+    attribute = AttributeValue.longAttributeValue(123456L);
+    assertThat(attribute.toString()).contains("123456");
+    attribute = AttributeValue.doubleAttributeValue(1.23456);
+    assertThat(attribute.toString()).contains("1.23456");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/BlankSpanTest.java b/api/src/test/java/io/opencensus/trace/BlankSpanTest.java
new file mode 100644
index 0000000..185a5ac
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/BlankSpanTest.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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BlankSpan}. */
+@RunWith(JUnit4.class)
+public class BlankSpanTest {
+  @Test
+  public void hasInvalidContextAndDefaultSpanOptions() {
+    assertThat(BlankSpan.INSTANCE.getContext()).isEqualTo(SpanContext.INVALID);
+    assertThat(BlankSpan.INSTANCE.getOptions().isEmpty()).isTrue();
+  }
+
+  @Test
+  public void doNotCrash() {
+    Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+    attributes.put(
+        "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+    Map<String, AttributeValue> multipleAttributes = new HashMap<String, AttributeValue>();
+    multipleAttributes.put(
+        "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+    multipleAttributes.put("MyBooleanAttributeKey", AttributeValue.booleanAttributeValue(true));
+    multipleAttributes.put("MyLongAttributeKey", AttributeValue.longAttributeValue(123));
+    // Tests only that all the methods are not crashing/throwing errors.
+    BlankSpan.INSTANCE.putAttribute(
+        "MyStringAttributeKey2", AttributeValue.stringAttributeValue("MyStringAttributeValue2"));
+    BlankSpan.INSTANCE.addAttributes(attributes);
+    BlankSpan.INSTANCE.addAttributes(multipleAttributes);
+    BlankSpan.INSTANCE.addAnnotation("MyAnnotation");
+    BlankSpan.INSTANCE.addAnnotation("MyAnnotation", attributes);
+    BlankSpan.INSTANCE.addAnnotation("MyAnnotation", multipleAttributes);
+    BlankSpan.INSTANCE.addAnnotation(Annotation.fromDescription("MyAnnotation"));
+    BlankSpan.INSTANCE.addNetworkEvent(NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).build());
+    BlankSpan.INSTANCE.addMessageEvent(MessageEvent.builder(MessageEvent.Type.SENT, 1L).build());
+    BlankSpan.INSTANCE.addLink(
+        Link.fromSpanContext(SpanContext.INVALID, Link.Type.CHILD_LINKED_SPAN));
+    BlankSpan.INSTANCE.setStatus(Status.OK);
+    BlankSpan.INSTANCE.end(EndSpanOptions.DEFAULT);
+    BlankSpan.INSTANCE.end();
+  }
+
+  @Test
+  public void blankSpan_ToString() {
+    assertThat(BlankSpan.INSTANCE.toString()).isEqualTo("BlankSpan");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/CurrentSpanUtilsTest.java b/api/src/test/java/io/opencensus/trace/CurrentSpanUtilsTest.java
new file mode 100644
index 0000000..6b16c3d
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/CurrentSpanUtilsTest.java
@@ -0,0 +1,294 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.trace.unsafe.ContextUtils;
+import java.util.concurrent.Callable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link CurrentSpanUtils}. */
+@RunWith(JUnit4.class)
+public class CurrentSpanUtilsTest {
+  @Mock private Span span;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  // TODO(bdrutu): When update to junit 4.13 use assertThrows instead of this.
+  private void executeRunnableAndExpectError(Runnable runnable, Throwable error) {
+    boolean called = false;
+    try {
+      CurrentSpanUtils.withSpan(span, true, runnable).run();
+    } catch (Throwable e) {
+      assertThat(e).isEqualTo(error);
+      called = true;
+    }
+    assertThat(called).isTrue();
+  }
+
+  // TODO(bdrutu): When update to junit 4.13 use assertThrows instead of this.
+  private void executeCallableAndExpectError(Callable<Object> callable, Throwable error) {
+    boolean called = false;
+    try {
+      CurrentSpanUtils.withSpan(span, true, callable).call();
+    } catch (Throwable e) {
+      assertThat(e).isEqualTo(error);
+      called = true;
+    }
+    assertThat(called).isTrue();
+  }
+
+  @Test
+  public void getCurrentSpan_WhenNoContext() {
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void getCurrentSpan() {
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Context origContext = Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, span).attach();
+    // Make sure context is detached even if test fails.
+    try {
+      assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+    } finally {
+      Context.current().detach(origContext);
+    }
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpan_CloseDetaches() {
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Scope ws = CurrentSpanUtils.withSpan(span, false);
+    try {
+      assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+    } finally {
+      ws.close();
+    }
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    verifyZeroInteractions(span);
+  }
+
+  @Test
+  public void withSpan_CloseDetachesAndEndsSpan() {
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Scope ss = CurrentSpanUtils.withSpan(span, true);
+    try {
+      assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+    } finally {
+      ss.close();
+    }
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    verify(span).end(same(EndSpanOptions.DEFAULT));
+  }
+
+  @Test
+  public void withSpanRunnable() {
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Runnable runnable =
+        new Runnable() {
+          @Override
+          public void run() {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+          }
+        };
+    CurrentSpanUtils.withSpan(span, false, runnable).run();
+    verifyZeroInteractions(span);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpanRunnable_EndSpan() {
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Runnable runnable =
+        new Runnable() {
+          @Override
+          public void run() {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+          }
+        };
+    CurrentSpanUtils.withSpan(span, true, runnable).run();
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpanRunnable_WithError() {
+    final AssertionError error = new AssertionError("MyError");
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Runnable runnable =
+        new Runnable() {
+          @Override
+          public void run() {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+            throw error;
+          }
+        };
+    executeRunnableAndExpectError(runnable, error);
+    verify(span).setStatus(Status.UNKNOWN.withDescription("MyError"));
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpanRunnable_WithErrorNoMessage() {
+    final AssertionError error = new AssertionError();
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Runnable runnable =
+        new Runnable() {
+          @Override
+          public void run() {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+            throw error;
+          }
+        };
+    executeRunnableAndExpectError(runnable, error);
+    verify(span).setStatus(Status.UNKNOWN.withDescription("AssertionError"));
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpanCallable() throws Exception {
+    final Object ret = new Object();
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Callable<Object> callable =
+        new Callable<Object>() {
+          @Override
+          public Object call() throws Exception {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+            return ret;
+          }
+        };
+    assertThat(CurrentSpanUtils.withSpan(span, false, callable).call()).isEqualTo(ret);
+    verifyZeroInteractions(span);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpanCallable_EndSpan() throws Exception {
+    final Object ret = new Object();
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Callable<Object> callable =
+        new Callable<Object>() {
+          @Override
+          public Object call() throws Exception {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+            return ret;
+          }
+        };
+    assertThat(CurrentSpanUtils.withSpan(span, true, callable).call()).isEqualTo(ret);
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpanCallable_WithException() {
+    final Exception exception = new Exception("MyException");
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Callable<Object> callable =
+        new Callable<Object>() {
+          @Override
+          public Object call() throws Exception {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+            throw exception;
+          }
+        };
+    executeCallableAndExpectError(callable, exception);
+    verify(span).setStatus(Status.UNKNOWN.withDescription("MyException"));
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpanCallable_WithExceptionNoMessage() {
+    final Exception exception = new Exception();
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Callable<Object> callable =
+        new Callable<Object>() {
+          @Override
+          public Object call() throws Exception {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+            throw exception;
+          }
+        };
+    executeCallableAndExpectError(callable, exception);
+    verify(span).setStatus(Status.UNKNOWN.withDescription("Exception"));
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpanCallable_WithError() {
+    final AssertionError error = new AssertionError("MyError");
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Callable<Object> callable =
+        new Callable<Object>() {
+          @Override
+          public Object call() throws Exception {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+            throw error;
+          }
+        };
+    executeCallableAndExpectError(callable, error);
+    verify(span).setStatus(Status.UNKNOWN.withDescription("MyError"));
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+
+  @Test
+  public void withSpanCallable_WithErrorNoMessage() {
+    final AssertionError error = new AssertionError();
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+    Callable<Object> callable =
+        new Callable<Object>() {
+          @Override
+          public Object call() throws Exception {
+            // When we run the runnable we will have the span in the current Context.
+            assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+            throw error;
+          }
+        };
+    executeCallableAndExpectError(callable, error);
+    verify(span).setStatus(Status.UNKNOWN.withDescription("AssertionError"));
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/EndSpanOptionsTest.java b/api/src/test/java/io/opencensus/trace/EndSpanOptionsTest.java
new file mode 100644
index 0000000..b6ab8e0
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/EndSpanOptionsTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link EndSpanOptions}. */
+@RunWith(JUnit4.class)
+public class EndSpanOptionsTest {
+  @Test
+  public void endSpanOptions_DefaultOptions() {
+    assertThat(EndSpanOptions.DEFAULT.getStatus()).isNull();
+    assertThat(EndSpanOptions.DEFAULT.getSampleToLocalSpanStore()).isFalse();
+  }
+
+  @Test
+  public void setStatus_Ok() {
+    EndSpanOptions endSpanOptions = EndSpanOptions.builder().setStatus(Status.OK).build();
+    assertThat(endSpanOptions.getStatus()).isEqualTo(Status.OK);
+  }
+
+  @Test
+  public void setStatus_Error() {
+    EndSpanOptions endSpanOptions =
+        EndSpanOptions.builder()
+            .setStatus(Status.CANCELLED.withDescription("ThisIsAnError"))
+            .build();
+    assertThat(endSpanOptions.getStatus())
+        .isEqualTo(Status.CANCELLED.withDescription("ThisIsAnError"));
+  }
+
+  @Test
+  public void setSampleToLocalSpanStore() {
+    EndSpanOptions endSpanOptions =
+        EndSpanOptions.builder().setSampleToLocalSpanStore(true).build();
+    assertThat(endSpanOptions.getSampleToLocalSpanStore()).isTrue();
+  }
+
+  @Test
+  public void endSpanOptions_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    tester.addEqualityGroup(
+        EndSpanOptions.builder()
+            .setStatus(Status.CANCELLED.withDescription("ThisIsAnError"))
+            .build(),
+        EndSpanOptions.builder()
+            .setStatus(Status.CANCELLED.withDescription("ThisIsAnError"))
+            .build());
+    tester.addEqualityGroup(EndSpanOptions.builder().build(), EndSpanOptions.DEFAULT);
+    tester.testEquals();
+  }
+
+  @Test
+  public void endSpanOptions_ToString() {
+    EndSpanOptions endSpanOptions =
+        EndSpanOptions.builder()
+            .setStatus(Status.CANCELLED.withDescription("ThisIsAnError"))
+            .build();
+    assertThat(endSpanOptions.toString()).contains("ThisIsAnError");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/LinkTest.java b/api/src/test/java/io/opencensus/trace/LinkTest.java
new file mode 100644
index 0000000..5c1ebf5
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/LinkTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.trace.Link.Type;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Link}. */
+@RunWith(JUnit4.class)
+public class LinkTest {
+  private final Map<String, AttributeValue> attributesMap = new HashMap<String, AttributeValue>();
+  private final Random random = new Random(1234);
+  private final SpanContext spanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+
+  @Before
+  public void setUp() {
+    attributesMap.put("MyAttributeKey0", AttributeValue.stringAttributeValue("MyStringAttribute"));
+    attributesMap.put("MyAttributeKey1", AttributeValue.longAttributeValue(10));
+    attributesMap.put("MyAttributeKey2", AttributeValue.booleanAttributeValue(true));
+  }
+
+  @Test
+  public void fromSpanContext_ChildLink() {
+    Link link = Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN);
+    assertThat(link.getTraceId()).isEqualTo(spanContext.getTraceId());
+    assertThat(link.getSpanId()).isEqualTo(spanContext.getSpanId());
+    assertThat(link.getType()).isEqualTo(Type.CHILD_LINKED_SPAN);
+  }
+
+  @Test
+  public void fromSpanContext_ChildLink_WithAttributes() {
+    Link link = Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN, attributesMap);
+    assertThat(link.getTraceId()).isEqualTo(spanContext.getTraceId());
+    assertThat(link.getSpanId()).isEqualTo(spanContext.getSpanId());
+    assertThat(link.getType()).isEqualTo(Type.CHILD_LINKED_SPAN);
+    assertThat(link.getAttributes()).isEqualTo(attributesMap);
+  }
+
+  @Test
+  public void fromSpanContext_ParentLink() {
+    Link link = Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN);
+    assertThat(link.getTraceId()).isEqualTo(spanContext.getTraceId());
+    assertThat(link.getSpanId()).isEqualTo(spanContext.getSpanId());
+    assertThat(link.getType()).isEqualTo(Type.PARENT_LINKED_SPAN);
+  }
+
+  @Test
+  public void fromSpanContext_ParentLink_WithAttributes() {
+    Link link = Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN, attributesMap);
+    assertThat(link.getTraceId()).isEqualTo(spanContext.getTraceId());
+    assertThat(link.getSpanId()).isEqualTo(spanContext.getSpanId());
+    assertThat(link.getType()).isEqualTo(Type.PARENT_LINKED_SPAN);
+    assertThat(link.getAttributes()).isEqualTo(attributesMap);
+  }
+
+  @Test
+  public void link_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    tester
+        .addEqualityGroup(
+            Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN),
+            Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN))
+        .addEqualityGroup(
+            Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN),
+            Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN))
+        .addEqualityGroup(Link.fromSpanContext(SpanContext.INVALID, Type.CHILD_LINKED_SPAN))
+        .addEqualityGroup(Link.fromSpanContext(SpanContext.INVALID, Type.PARENT_LINKED_SPAN))
+        .addEqualityGroup(
+            Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN, attributesMap),
+            Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN, attributesMap));
+    tester.testEquals();
+  }
+
+  @Test
+  public void link_ToString() {
+    Link link = Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN, attributesMap);
+    assertThat(link.toString()).contains(spanContext.getTraceId().toString());
+    assertThat(link.toString()).contains(spanContext.getSpanId().toString());
+    assertThat(link.toString()).contains("CHILD_LINKED_SPAN");
+    assertThat(link.toString()).contains(attributesMap.toString());
+    link = Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN, attributesMap);
+    assertThat(link.toString()).contains(spanContext.getTraceId().toString());
+    assertThat(link.toString()).contains(spanContext.getSpanId().toString());
+    assertThat(link.toString()).contains("PARENT_LINKED_SPAN");
+    assertThat(link.toString()).contains(attributesMap.toString());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/LowerCaseBase16EncodingTest.java b/api/src/test/java/io/opencensus/trace/LowerCaseBase16EncodingTest.java
new file mode 100644
index 0000000..3444d2b
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/LowerCaseBase16EncodingTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.nio.charset.Charset;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link io.opencensus.trace.LowerCaseBase16Encoding}. */
+@RunWith(JUnit4.class)
+public class LowerCaseBase16EncodingTest {
+  private static final Charset CHARSET = Charset.forName("UTF-8");
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void valid_EncodeDecode() {
+    testEncoding("", "");
+    testEncoding("f", "66");
+    testEncoding("fo", "666f");
+    testEncoding("foo", "666f6f");
+    testEncoding("foob", "666f6f62");
+    testEncoding("fooba", "666f6f6261");
+    testEncoding("foobar", "666f6f626172");
+  }
+
+  @Test
+  public void invalidDecodings_UnrecongnizedCharacters() {
+    // These contain bytes not in the decoding.
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Invalid character g");
+    LowerCaseBase16Encoding.decodeToBytes("efhg");
+  }
+
+  @Test
+  public void invalidDecodings_InvalidInputLength() {
+    // Valid base16 strings always have an even length.
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Invalid input length 3");
+    LowerCaseBase16Encoding.decodeToBytes("abc");
+  }
+
+  @Test
+  public void invalidDecodings_InvalidInputLengthAndCharacter() {
+    // These have a combination of invalid length and unrecognized characters.
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Invalid input length 1");
+    LowerCaseBase16Encoding.decodeToBytes("?");
+  }
+
+  private static void testEncoding(String decoded, String encoded) {
+    testEncodes(decoded, encoded);
+    testDecodes(encoded, decoded);
+  }
+
+  private static void testEncodes(String decoded, String encoded) {
+    assertThat(LowerCaseBase16Encoding.encodeToString(decoded.getBytes(CHARSET)))
+        .isEqualTo(encoded);
+  }
+
+  private static void testDecodes(String encoded, String decoded) {
+    assertThat(LowerCaseBase16Encoding.decodeToBytes(encoded)).isEqualTo(decoded.getBytes(CHARSET));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/MessageEventTest.java b/api/src/test/java/io/opencensus/trace/MessageEventTest.java
new file mode 100644
index 0000000..fde32fe
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/MessageEventTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MessageEvent}. */
+@RunWith(JUnit4.class)
+public class MessageEventTest {
+  @Test(expected = NullPointerException.class)
+  public void buildMessageEvent_NullType() {
+    MessageEvent.builder(null, 1L).build();
+  }
+
+  @Test
+  public void buildMessageEvent_WithRequiredFields() {
+    MessageEvent messageEvent = MessageEvent.builder(MessageEvent.Type.SENT, 1L).build();
+    assertThat(messageEvent.getType()).isEqualTo(MessageEvent.Type.SENT);
+    assertThat(messageEvent.getMessageId()).isEqualTo(1L);
+    assertThat(messageEvent.getUncompressedMessageSize()).isEqualTo(0L);
+  }
+
+  @Test
+  public void buildMessageEvent_WithUncompressedMessageSize() {
+    MessageEvent messageEvent =
+        MessageEvent.builder(MessageEvent.Type.SENT, 1L).setUncompressedMessageSize(123L).build();
+    assertThat(messageEvent.getType()).isEqualTo(MessageEvent.Type.SENT);
+    assertThat(messageEvent.getMessageId()).isEqualTo(1L);
+    assertThat(messageEvent.getUncompressedMessageSize()).isEqualTo(123L);
+  }
+
+  @Test
+  public void buildMessageEvent_WithCompressedMessageSize() {
+    MessageEvent messageEvent =
+        MessageEvent.builder(MessageEvent.Type.SENT, 1L).setCompressedMessageSize(123L).build();
+    assertThat(messageEvent.getType()).isEqualTo(MessageEvent.Type.SENT);
+    assertThat(messageEvent.getMessageId()).isEqualTo(1L);
+    assertThat(messageEvent.getCompressedMessageSize()).isEqualTo(123L);
+  }
+
+  @Test
+  public void buildMessageEvent_WithAllValues() {
+    MessageEvent messageEvent =
+        MessageEvent.builder(MessageEvent.Type.RECEIVED, 1L)
+            .setUncompressedMessageSize(123L)
+            .setCompressedMessageSize(63L)
+            .build();
+    assertThat(messageEvent.getType()).isEqualTo(MessageEvent.Type.RECEIVED);
+    assertThat(messageEvent.getMessageId()).isEqualTo(1L);
+    assertThat(messageEvent.getUncompressedMessageSize()).isEqualTo(123L);
+    assertThat(messageEvent.getCompressedMessageSize()).isEqualTo(63L);
+  }
+
+  @Test
+  public void messageEvent_ToString() {
+    MessageEvent messageEvent =
+        MessageEvent.builder(MessageEvent.Type.SENT, 1L)
+            .setUncompressedMessageSize(123L)
+            .setCompressedMessageSize(63L)
+            .build();
+    assertThat(messageEvent.toString()).contains("type=SENT");
+    assertThat(messageEvent.toString()).contains("messageId=1");
+    assertThat(messageEvent.toString()).contains("compressedMessageSize=63");
+    assertThat(messageEvent.toString()).contains("uncompressedMessageSize=123");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/NetworkEventTest.java b/api/src/test/java/io/opencensus/trace/NetworkEventTest.java
new file mode 100644
index 0000000..8c13237
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/NetworkEventTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link NetworkEvent}. */
+@RunWith(JUnit4.class)
+public class NetworkEventTest {
+  @Test(expected = NullPointerException.class)
+  public void buildNetworkEvent_NullType() {
+    NetworkEvent.builder(null, 1L).build();
+  }
+
+  @Test
+  public void buildNetworkEvent_WithRequiredFields() {
+    NetworkEvent networkEvent = NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).build();
+    assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+    assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+    assertThat(networkEvent.getKernelTimestamp()).isNull();
+    assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(0L);
+  }
+
+  @Test
+  public void buildNetworkEvent_WithTimestamp() {
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.SENT, 1L)
+            .setKernelTimestamp(Timestamp.fromMillis(123456L))
+            .build();
+    assertThat(networkEvent.getKernelTimestamp()).isEqualTo(Timestamp.fromMillis(123456L));
+    assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+    assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+    assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(0L);
+  }
+
+  @Test
+  public void buildNetworkEvent_WithUncompressedMessageSize() {
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).setUncompressedMessageSize(123L).build();
+    assertThat(networkEvent.getKernelTimestamp()).isNull();
+    assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+    assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+    assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(123L);
+    assertThat(networkEvent.getMessageSize()).isEqualTo(123L);
+  }
+
+  @Test
+  public void buildNetworkEvent_WithMessageSize() {
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).setMessageSize(123L).build();
+    assertThat(networkEvent.getKernelTimestamp()).isNull();
+    assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+    assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+    assertThat(networkEvent.getMessageSize()).isEqualTo(123L);
+    assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(123L);
+  }
+
+  @Test
+  public void buildNetworkEvent_WithCompressedMessageSize() {
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).setCompressedMessageSize(123L).build();
+    assertThat(networkEvent.getKernelTimestamp()).isNull();
+    assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+    assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+    assertThat(networkEvent.getCompressedMessageSize()).isEqualTo(123L);
+  }
+
+  @Test
+  public void buildNetworkEvent_WithAllValues() {
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.RECV, 1L)
+            .setKernelTimestamp(Timestamp.fromMillis(123456L))
+            .setUncompressedMessageSize(123L)
+            .setCompressedMessageSize(63L)
+            .build();
+    assertThat(networkEvent.getKernelTimestamp()).isEqualTo(Timestamp.fromMillis(123456L));
+    assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.RECV);
+    assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+    assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(123L);
+    // Test that getMessageSize returns same as getUncompressedMessageSize();
+    assertThat(networkEvent.getMessageSize()).isEqualTo(123L);
+    assertThat(networkEvent.getCompressedMessageSize()).isEqualTo(63L);
+  }
+
+  @Test
+  public void networkEvent_ToString() {
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.SENT, 1L)
+            .setKernelTimestamp(Timestamp.fromMillis(123456L))
+            .setUncompressedMessageSize(123L)
+            .setCompressedMessageSize(63L)
+            .build();
+    assertThat(networkEvent.toString()).contains(Timestamp.fromMillis(123456L).toString());
+    assertThat(networkEvent.toString()).contains("type=SENT");
+    assertThat(networkEvent.toString()).contains("messageId=1");
+    assertThat(networkEvent.toString()).contains("compressedMessageSize=63");
+    assertThat(networkEvent.toString()).contains("uncompressedMessageSize=123");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/NoopSpan.java b/api/src/test/java/io/opencensus/trace/NoopSpan.java
new file mode 100644
index 0000000..a21a8aa
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/NoopSpan.java
@@ -0,0 +1,69 @@
+/*
+ * 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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.EnumSet;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Class to be used in tests where an implementation for the Span is needed.
+ *
+ * <p>Not final to allow Mockito to "spy" this class.
+ */
+public class NoopSpan extends Span {
+
+  /** Creates a new {@code NoopSpan}. */
+  public NoopSpan(SpanContext context, @Nullable EnumSet<Options> options) {
+    super(Utils.checkNotNull(context, "context"), options);
+  }
+
+  @Override
+  public void putAttributes(Map<String, AttributeValue> attributes) {
+    Utils.checkNotNull(attributes, "attributes");
+  }
+
+  @Override
+  public void addAnnotation(String description, Map<String, AttributeValue> attributes) {
+    Utils.checkNotNull(description, "description");
+    Utils.checkNotNull(attributes, "attributes");
+  }
+
+  @Override
+  public void addAnnotation(Annotation annotation) {
+    Utils.checkNotNull(annotation, "annotation");
+  }
+
+  @Override
+  public void addNetworkEvent(NetworkEvent networkEvent) {}
+
+  @Override
+  public void addMessageEvent(MessageEvent messageEvent) {
+    Utils.checkNotNull(messageEvent, "messageEvent");
+  }
+
+  @Override
+  public void addLink(Link link) {
+    Utils.checkNotNull(link, "link");
+  }
+
+  @Override
+  public void end(EndSpanOptions options) {
+    Utils.checkNotNull(options, "options");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/SpanBuilderTest.java b/api/src/test/java/io/opencensus/trace/SpanBuilderTest.java
new file mode 100644
index 0000000..839c894
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/SpanBuilderTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.common.Scope;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collections;
+import java.util.concurrent.Callable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link SpanBuilder}. */
+@RunWith(JUnit4.class)
+// Need to suppress warnings for MustBeClosed because Java-6 does not support try-with-resources.
+@SuppressWarnings("MustBeClosedChecker")
+public class SpanBuilderTest {
+  private final Tracer tracer = Tracing.getTracer();
+  @Mock private SpanBuilder spanBuilder;
+  @Mock private Span span;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    when(spanBuilder.startSpan()).thenReturn(span);
+  }
+
+  @Test
+  public void startScopedSpan() {
+    assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+    Scope scope = spanBuilder.startScopedSpan();
+    try {
+      assertThat(tracer.getCurrentSpan()).isSameAs(span);
+    } finally {
+      scope.close();
+    }
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test
+  public void startSpanAndRun() {
+    assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+    spanBuilder.startSpanAndRun(
+        new Runnable() {
+          @Override
+          public void run() {
+            assertThat(tracer.getCurrentSpan()).isSameAs(span);
+          }
+        });
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test
+  public void startSpanAndCall() throws Exception {
+    final Object ret = new Object();
+    assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+    assertThat(
+            spanBuilder.startSpanAndCall(
+                new Callable<Object>() {
+                  @Override
+                  public Object call() throws Exception {
+                    assertThat(tracer.getCurrentSpan()).isSameAs(span);
+                    return ret;
+                  }
+                }))
+        .isEqualTo(ret);
+    verify(span).end(EndSpanOptions.DEFAULT);
+    assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test
+  public void doNotCrash_NoopImplementation() throws Exception {
+    SpanBuilder spanBuilder = tracer.spanBuilder("MySpanName");
+    spanBuilder.setParentLinks(Collections.<Span>emptyList());
+    spanBuilder.setRecordEvents(true);
+    spanBuilder.setSampler(Samplers.alwaysSample());
+    spanBuilder.setSpanKind(Kind.SERVER);
+    assertThat(spanBuilder.startSpan()).isSameAs(BlankSpan.INSTANCE);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/SpanContextTest.java b/api/src/test/java/io/opencensus/trace/SpanContextTest.java
new file mode 100644
index 0000000..54e188c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/SpanContextTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SpanContext}. */
+@RunWith(JUnit4.class)
+public class SpanContextTest {
+  private static final byte[] firstTraceIdBytes =
+      new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'a'};
+  private static final byte[] secondTraceIdBytes =
+      new byte[] {0, 0, 0, 0, 0, 0, 0, '0', 0, 0, 0, 0, 0, 0, 0, 0};
+  private static final byte[] firstSpanIdBytes = new byte[] {0, 0, 0, 0, 0, 0, 0, 'a'};
+  private static final byte[] secondSpanIdBytes = new byte[] {'0', 0, 0, 0, 0, 0, 0, 0};
+  private static final Tracestate firstTracestate = Tracestate.builder().set("foo", "bar").build();
+  private static final Tracestate secondTracestate = Tracestate.builder().set("foo", "baz").build();
+  private static final SpanContext first =
+      SpanContext.create(
+          TraceId.fromBytes(firstTraceIdBytes),
+          SpanId.fromBytes(firstSpanIdBytes),
+          TraceOptions.DEFAULT,
+          firstTracestate);
+  private static final SpanContext second =
+      SpanContext.create(
+          TraceId.fromBytes(secondTraceIdBytes),
+          SpanId.fromBytes(secondSpanIdBytes),
+          TraceOptions.builder().setIsSampled(true).build(),
+          secondTracestate);
+
+  @Test
+  public void invalidSpanContext() {
+    assertThat(SpanContext.INVALID.getTraceId()).isEqualTo(TraceId.INVALID);
+    assertThat(SpanContext.INVALID.getSpanId()).isEqualTo(SpanId.INVALID);
+    assertThat(SpanContext.INVALID.getTraceOptions()).isEqualTo(TraceOptions.DEFAULT);
+  }
+
+  @Test
+  public void isValid() {
+    assertThat(SpanContext.INVALID.isValid()).isFalse();
+    assertThat(
+            SpanContext.create(
+                    TraceId.fromBytes(firstTraceIdBytes), SpanId.INVALID, TraceOptions.DEFAULT)
+                .isValid())
+        .isFalse();
+    assertThat(
+            SpanContext.create(
+                    TraceId.INVALID, SpanId.fromBytes(firstSpanIdBytes), TraceOptions.DEFAULT)
+                .isValid())
+        .isFalse();
+    assertThat(first.isValid()).isTrue();
+    assertThat(second.isValid()).isTrue();
+  }
+
+  @Test
+  public void getTraceId() {
+    assertThat(first.getTraceId()).isEqualTo(TraceId.fromBytes(firstTraceIdBytes));
+    assertThat(second.getTraceId()).isEqualTo(TraceId.fromBytes(secondTraceIdBytes));
+  }
+
+  @Test
+  public void getSpanId() {
+    assertThat(first.getSpanId()).isEqualTo(SpanId.fromBytes(firstSpanIdBytes));
+    assertThat(second.getSpanId()).isEqualTo(SpanId.fromBytes(secondSpanIdBytes));
+  }
+
+  @Test
+  public void getTraceOptions() {
+    assertThat(first.getTraceOptions()).isEqualTo(TraceOptions.DEFAULT);
+    assertThat(second.getTraceOptions())
+        .isEqualTo(TraceOptions.builder().setIsSampled(true).build());
+  }
+
+  @Test
+  public void getTracestate() {
+    assertThat(first.getTracestate()).isEqualTo(firstTracestate);
+    assertThat(second.getTracestate()).isEqualTo(secondTracestate);
+  }
+
+  @Test
+  public void spanContext_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    tester.addEqualityGroup(
+        first,
+        SpanContext.create(
+            TraceId.fromBytes(firstTraceIdBytes),
+            SpanId.fromBytes(firstSpanIdBytes),
+            TraceOptions.DEFAULT),
+        SpanContext.create(
+            TraceId.fromBytes(firstTraceIdBytes),
+            SpanId.fromBytes(firstSpanIdBytes),
+            TraceOptions.builder().setIsSampled(false).build(),
+            firstTracestate));
+    tester.addEqualityGroup(
+        second,
+        SpanContext.create(
+            TraceId.fromBytes(secondTraceIdBytes),
+            SpanId.fromBytes(secondSpanIdBytes),
+            TraceOptions.builder().setIsSampled(true).build(),
+            secondTracestate));
+    tester.testEquals();
+  }
+
+  @Test
+  public void spanContext_ToString() {
+    assertThat(first.toString()).contains(TraceId.fromBytes(firstTraceIdBytes).toString());
+    assertThat(first.toString()).contains(SpanId.fromBytes(firstSpanIdBytes).toString());
+    assertThat(first.toString()).contains(TraceOptions.DEFAULT.toString());
+    assertThat(second.toString()).contains(TraceId.fromBytes(secondTraceIdBytes).toString());
+    assertThat(second.toString()).contains(SpanId.fromBytes(secondSpanIdBytes).toString());
+    assertThat(second.toString())
+        .contains(TraceOptions.builder().setIsSampled(true).build().toString());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/SpanIdTest.java b/api/src/test/java/io/opencensus/trace/SpanIdTest.java
new file mode 100644
index 0000000..4a5bc2a
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/SpanIdTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SpanId}. */
+@RunWith(JUnit4.class)
+public class SpanIdTest {
+  private static final byte[] firstBytes = new byte[] {0, 0, 0, 0, 0, 0, 0, 'a'};
+  private static final byte[] secondBytes = new byte[] {(byte) 0xFF, 0, 0, 0, 0, 0, 0, 'A'};
+  private static final SpanId first = SpanId.fromBytes(firstBytes);
+  private static final SpanId second = SpanId.fromBytes(secondBytes);
+
+  @Test
+  public void invalidSpanId() {
+    assertThat(SpanId.INVALID.getBytes()).isEqualTo(new byte[8]);
+  }
+
+  @Test
+  public void isValid() {
+    assertThat(SpanId.INVALID.isValid()).isFalse();
+    assertThat(first.isValid()).isTrue();
+    assertThat(second.isValid()).isTrue();
+  }
+
+  @Test
+  public void fromLowerBase16() {
+    assertThat(SpanId.fromLowerBase16("0000000000000000")).isEqualTo(SpanId.INVALID);
+    assertThat(SpanId.fromLowerBase16("0000000000000061")).isEqualTo(first);
+    assertThat(SpanId.fromLowerBase16("ff00000000000041")).isEqualTo(second);
+  }
+
+  @Test
+  public void toLowerBase16() {
+    assertThat(SpanId.INVALID.toLowerBase16()).isEqualTo("0000000000000000");
+    assertThat(first.toLowerBase16()).isEqualTo("0000000000000061");
+    assertThat(second.toLowerBase16()).isEqualTo("ff00000000000041");
+  }
+
+  @Test
+  public void getBytes() {
+    assertThat(first.getBytes()).isEqualTo(firstBytes);
+    assertThat(second.getBytes()).isEqualTo(secondBytes);
+  }
+
+  @Test
+  public void traceId_CompareTo() {
+    assertThat(first.compareTo(second)).isGreaterThan(0);
+    assertThat(second.compareTo(first)).isLessThan(0);
+    assertThat(first.compareTo(SpanId.fromBytes(firstBytes))).isEqualTo(0);
+  }
+
+  @Test
+  public void traceId_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    tester.addEqualityGroup(SpanId.INVALID, SpanId.INVALID);
+    tester.addEqualityGroup(first, SpanId.fromBytes(Arrays.copyOf(firstBytes, firstBytes.length)));
+    tester.addEqualityGroup(
+        second, SpanId.fromBytes(Arrays.copyOf(secondBytes, secondBytes.length)));
+    tester.testEquals();
+  }
+
+  @Test
+  public void traceId_ToString() {
+    assertThat(SpanId.INVALID.toString()).contains("0000000000000000");
+    assertThat(first.toString()).contains("0000000000000061");
+    assertThat(second.toString()).contains("ff00000000000041");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/SpanTest.java b/api/src/test/java/io/opencensus/trace/SpanTest.java
new file mode 100644
index 0000000..f7546ca
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/SpanTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link Span}. */
+@RunWith(JUnit4.class)
+public class SpanTest {
+  private Random random;
+  private SpanContext spanContext;
+  private SpanContext notSampledSpanContext;
+  private EnumSet<Span.Options> spanOptions;
+
+  @Before
+  public void setUp() {
+    random = new Random(1234);
+    spanContext =
+        SpanContext.create(
+            TraceId.generateRandomId(random),
+            SpanId.generateRandomId(random),
+            TraceOptions.builder().setIsSampled(true).build());
+    notSampledSpanContext =
+        SpanContext.create(
+            TraceId.generateRandomId(random),
+            SpanId.generateRandomId(random),
+            TraceOptions.DEFAULT);
+    spanOptions = EnumSet.of(Span.Options.RECORD_EVENTS);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void newSpan_WithNullContext() {
+    new NoopSpan(null, null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void newSpan_SampledContextAndNullOptions() {
+    new NoopSpan(spanContext, null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void newSpan_SampledContextAndEmptyOptions() {
+    new NoopSpan(spanContext, EnumSet.noneOf(Span.Options.class));
+  }
+
+  @Test
+  public void getOptions_WhenNullOptions() {
+    Span span = new NoopSpan(notSampledSpanContext, null);
+    assertThat(span.getOptions()).isEmpty();
+  }
+
+  @Test
+  public void getContextAndOptions() {
+    Span span = new NoopSpan(spanContext, spanOptions);
+    assertThat(span.getContext()).isEqualTo(spanContext);
+    assertThat(span.getOptions()).isEqualTo(spanOptions);
+  }
+
+  @Test
+  public void putAttributeCallsAddAttributesByDefault() {
+    Span span = Mockito.spy(new NoopSpan(spanContext, spanOptions));
+    span.putAttribute("MyKey", AttributeValue.booleanAttributeValue(true));
+    span.end();
+    verify(span)
+        .putAttributes(
+            eq(Collections.singletonMap("MyKey", AttributeValue.booleanAttributeValue(true))));
+  }
+
+  @Test
+  public void endCallsEndWithDefaultOptions() {
+    Span span = Mockito.spy(new NoopSpan(spanContext, spanOptions));
+    span.end();
+    verify(span).end(same(EndSpanOptions.DEFAULT));
+  }
+
+  @Test
+  public void addMessageEventDefaultImplementation() {
+    Span mockSpan = Mockito.mock(Span.class);
+    MessageEvent messageEvent =
+        MessageEvent.builder(MessageEvent.Type.SENT, 123)
+            .setUncompressedMessageSize(456)
+            .setCompressedMessageSize(789)
+            .build();
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.SENT, 123)
+            .setUncompressedMessageSize(456)
+            .setCompressedMessageSize(789)
+            .build();
+    Mockito.doCallRealMethod().when(mockSpan).addMessageEvent(messageEvent);
+    mockSpan.addMessageEvent(messageEvent);
+    verify(mockSpan).addNetworkEvent(eq(networkEvent));
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/StatusTest.java b/api/src/test/java/io/opencensus/trace/StatusTest.java
new file mode 100644
index 0000000..108db2d
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/StatusTest.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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Status}. */
+@RunWith(JUnit4.class)
+public class StatusTest {
+  @Test
+  public void status_Ok() {
+    assertThat(Status.OK.getCanonicalCode()).isEqualTo(Status.CanonicalCode.OK);
+    assertThat(Status.OK.getDescription()).isNull();
+    assertThat(Status.OK.isOk()).isTrue();
+  }
+
+  @Test
+  public void createStatus_WithDescription() {
+    Status status = Status.UNKNOWN.withDescription("This is an error.");
+    assertThat(status.getCanonicalCode()).isEqualTo(Status.CanonicalCode.UNKNOWN);
+    assertThat(status.getDescription()).isEqualTo("This is an error.");
+    assertThat(status.isOk()).isFalse();
+  }
+
+  @Test
+  public void status_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    tester.addEqualityGroup(Status.OK, Status.OK.withDescription(null));
+    tester.addEqualityGroup(
+        Status.CANCELLED.withDescription("ThisIsAnError"),
+        Status.CANCELLED.withDescription("ThisIsAnError"));
+    tester.addEqualityGroup(Status.UNKNOWN.withDescription("This is an error."));
+    tester.testEquals();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TraceComponentTest.java b/api/src/test/java/io/opencensus/trace/TraceComponentTest.java
new file mode 100644
index 0000000..1c3f07d
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TraceComponentTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.internal.ZeroTimeClock;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceComponent}. */
+@RunWith(JUnit4.class)
+public class TraceComponentTest {
+  @Test
+  public void defaultTracer() {
+    assertThat(TraceComponent.newNoopTraceComponent().getTracer()).isSameAs(Tracer.getNoopTracer());
+  }
+
+  @Test
+  public void defaultBinaryPropagationHandler() {
+    assertThat(TraceComponent.newNoopTraceComponent().getPropagationComponent())
+        .isSameAs(PropagationComponent.getNoopPropagationComponent());
+  }
+
+  @Test
+  public void defaultClock() {
+    assertThat(TraceComponent.newNoopTraceComponent().getClock()).isInstanceOf(ZeroTimeClock.class);
+  }
+
+  @Test
+  public void defaultTraceExporter() {
+    assertThat(TraceComponent.newNoopTraceComponent().getExportComponent())
+        .isInstanceOf(ExportComponent.newNoopExportComponent().getClass());
+  }
+
+  @Test
+  public void defaultTraceConfig() {
+    assertThat(TraceComponent.newNoopTraceComponent().getTraceConfig())
+        .isSameAs(TraceConfig.getNoopTraceConfig());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TraceIdTest.java b/api/src/test/java/io/opencensus/trace/TraceIdTest.java
new file mode 100644
index 0000000..c8b5dc8
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TraceIdTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceId}. */
+@RunWith(JUnit4.class)
+public class TraceIdTest {
+  private static final byte[] firstBytes =
+      new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'a'};
+  private static final byte[] secondBytes =
+      new byte[] {(byte) 0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'A'};
+  private static final TraceId first = TraceId.fromBytes(firstBytes);
+  private static final TraceId second = TraceId.fromBytes(secondBytes);
+
+  @Test
+  public void invalidTraceId() {
+    assertThat(TraceId.INVALID.getBytes()).isEqualTo(new byte[16]);
+  }
+
+  @Test
+  public void isValid() {
+    assertThat(TraceId.INVALID.isValid()).isFalse();
+    assertThat(first.isValid()).isTrue();
+    assertThat(second.isValid()).isTrue();
+  }
+
+  @Test
+  public void getBytes() {
+    assertThat(first.getBytes()).isEqualTo(firstBytes);
+    assertThat(second.getBytes()).isEqualTo(secondBytes);
+  }
+
+  @Test
+  public void fromLowerBase16() {
+    assertThat(TraceId.fromLowerBase16("00000000000000000000000000000000"))
+        .isEqualTo(TraceId.INVALID);
+    assertThat(TraceId.fromLowerBase16("00000000000000000000000000000061")).isEqualTo(first);
+    assertThat(TraceId.fromLowerBase16("ff000000000000000000000000000041")).isEqualTo(second);
+  }
+
+  @Test
+  public void toLowerBase16() {
+    assertThat(TraceId.INVALID.toLowerBase16()).isEqualTo("00000000000000000000000000000000");
+    assertThat(first.toLowerBase16()).isEqualTo("00000000000000000000000000000061");
+    assertThat(second.toLowerBase16()).isEqualTo("ff000000000000000000000000000041");
+  }
+
+  @Test
+  public void traceId_CompareTo() {
+    assertThat(first.compareTo(second)).isGreaterThan(0);
+    assertThat(second.compareTo(first)).isLessThan(0);
+    assertThat(first.compareTo(TraceId.fromBytes(firstBytes))).isEqualTo(0);
+  }
+
+  @Test
+  public void traceId_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    tester.addEqualityGroup(TraceId.INVALID, TraceId.INVALID);
+    tester.addEqualityGroup(first, TraceId.fromBytes(Arrays.copyOf(firstBytes, firstBytes.length)));
+    tester.addEqualityGroup(
+        second, TraceId.fromBytes(Arrays.copyOf(secondBytes, secondBytes.length)));
+    tester.testEquals();
+  }
+
+  @Test
+  public void traceId_ToString() {
+    assertThat(TraceId.INVALID.toString()).contains("00000000000000000000000000000000");
+    assertThat(first.toString()).contains("00000000000000000000000000000061");
+    assertThat(second.toString()).contains("ff000000000000000000000000000041");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TraceOptionsTest.java b/api/src/test/java/io/opencensus/trace/TraceOptionsTest.java
new file mode 100644
index 0000000..3c46d09
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TraceOptionsTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceOptions}. */
+@RunWith(JUnit4.class)
+public class TraceOptionsTest {
+  private static final byte FIRST_BYTE = (byte) 0xff;
+  private static final byte SECOND_BYTE = 1;
+  private static final byte THIRD_BYTE = 6;
+
+  @Test
+  public void getOptions() {
+    assertThat(TraceOptions.DEFAULT.getOptions()).isEqualTo(0);
+    assertThat(TraceOptions.builder().setIsSampled(false).build().getOptions()).isEqualTo(0);
+    assertThat(TraceOptions.builder().setIsSampled(true).build().getOptions()).isEqualTo(1);
+    assertThat(TraceOptions.builder().setIsSampled(true).setIsSampled(false).build().getOptions())
+        .isEqualTo(0);
+    assertThat(TraceOptions.fromByte(FIRST_BYTE).getOptions()).isEqualTo(-1);
+    assertThat(TraceOptions.fromByte(SECOND_BYTE).getOptions()).isEqualTo(1);
+    assertThat(TraceOptions.fromByte(THIRD_BYTE).getOptions()).isEqualTo(6);
+  }
+
+  @Test
+  public void isSampled() {
+    assertThat(TraceOptions.DEFAULT.isSampled()).isFalse();
+    assertThat(TraceOptions.builder().setIsSampled(true).build().isSampled()).isTrue();
+  }
+
+  @Test
+  public void toFromByte() {
+    assertThat(TraceOptions.fromByte(FIRST_BYTE).getByte()).isEqualTo(FIRST_BYTE);
+    assertThat(TraceOptions.fromByte(SECOND_BYTE).getByte()).isEqualTo(SECOND_BYTE);
+    assertThat(TraceOptions.fromByte(THIRD_BYTE).getByte()).isEqualTo(THIRD_BYTE);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void deprecated_fromBytes() {
+    assertThat(TraceOptions.fromBytes(new byte[] {FIRST_BYTE}).getByte()).isEqualTo(FIRST_BYTE);
+    assertThat(TraceOptions.fromBytes(new byte[] {1, FIRST_BYTE}, 1).getByte())
+        .isEqualTo(FIRST_BYTE);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void deprecated_getBytes() {
+    assertThat(TraceOptions.fromByte(FIRST_BYTE).getBytes()).isEqualTo(new byte[] {FIRST_BYTE});
+  }
+
+  @Test
+  public void builder_FromOptions() {
+    assertThat(
+            TraceOptions.builder(TraceOptions.fromByte(THIRD_BYTE))
+                .setIsSampled(true)
+                .build()
+                .getOptions())
+        .isEqualTo(6 | 1);
+  }
+
+  @Test
+  public void traceOptions_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    tester.addEqualityGroup(TraceOptions.DEFAULT);
+    tester.addEqualityGroup(
+        TraceOptions.fromByte(SECOND_BYTE), TraceOptions.builder().setIsSampled(true).build());
+    tester.addEqualityGroup(TraceOptions.fromByte(FIRST_BYTE));
+    tester.testEquals();
+  }
+
+  @Test
+  public void traceOptions_ToString() {
+    assertThat(TraceOptions.DEFAULT.toString()).contains("sampled=false");
+    assertThat(TraceOptions.builder().setIsSampled(true).build().toString())
+        .contains("sampled=true");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TracerTest.java b/api/src/test/java/io/opencensus/trace/TracerTest.java
new file mode 100644
index 0000000..58dd4bb
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TracerTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.common.Scope;
+import java.util.concurrent.Callable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link Tracer}. */
+@RunWith(JUnit4.class)
+// Need to suppress warnings for MustBeClosed because Java-6 does not support try-with-resources.
+@SuppressWarnings("MustBeClosedChecker")
+public class TracerTest {
+  private static final Tracer noopTracer = Tracer.getNoopTracer();
+  private static final String SPAN_NAME = "MySpanName";
+  @Mock private Tracer tracer;
+  @Mock private SpanBuilder spanBuilder;
+  @Mock private Span span;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void defaultGetCurrentSpan() {
+    assertThat(noopTracer.getCurrentSpan()).isEqualTo(BlankSpan.INSTANCE);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void withSpan_NullSpan() {
+    noopTracer.withSpan(null);
+  }
+
+  @Test
+  public void getCurrentSpan_WithSpan() {
+    assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+    Scope ws = noopTracer.withSpan(span);
+    try {
+      assertThat(noopTracer.getCurrentSpan()).isSameAs(span);
+    } finally {
+      ws.close();
+    }
+    assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test
+  public void wrapRunnable() {
+    Runnable runnable;
+    assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+    runnable =
+        tracer.withSpan(
+            span,
+            new Runnable() {
+              @Override
+              public void run() {
+                assertThat(noopTracer.getCurrentSpan()).isSameAs(span);
+              }
+            });
+    // When we run the runnable we will have the span in the current Context.
+    runnable.run();
+    verifyZeroInteractions(span);
+    assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test
+  public void wrapCallable() throws Exception {
+    final Object ret = new Object();
+    Callable<Object> callable;
+    assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+    callable =
+        tracer.withSpan(
+            span,
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                assertThat(noopTracer.getCurrentSpan()).isSameAs(span);
+                return ret;
+              }
+            });
+    // When we call the callable we will have the span in the current Context.
+    assertThat(callable.call()).isEqualTo(ret);
+    verifyZeroInteractions(span);
+    assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void spanBuilderWithName_NullName() {
+    noopTracer.spanBuilder(null);
+  }
+
+  @Test
+  public void defaultSpanBuilderWithName() {
+    assertThat(noopTracer.spanBuilder(SPAN_NAME).startSpan()).isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void spanBuilderWithParentAndName_NullName() {
+    noopTracer.spanBuilderWithExplicitParent(null, null);
+  }
+
+  @Test
+  public void defaultSpanBuilderWithParentAndName() {
+    assertThat(noopTracer.spanBuilderWithExplicitParent(SPAN_NAME, null).startSpan())
+        .isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void spanBuilderWithRemoteParent_NullName() {
+    noopTracer.spanBuilderWithRemoteParent(null, null);
+  }
+
+  @Test
+  public void defaultSpanBuilderWithRemoteParent_NullParent() {
+    assertThat(noopTracer.spanBuilderWithRemoteParent(SPAN_NAME, null).startSpan())
+        .isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test
+  public void defaultSpanBuilderWithRemoteParent() {
+    assertThat(noopTracer.spanBuilderWithRemoteParent(SPAN_NAME, SpanContext.INVALID).startSpan())
+        .isSameAs(BlankSpan.INSTANCE);
+  }
+
+  @Test
+  public void startSpanWithParentFromContext() {
+    Scope ws = tracer.withSpan(span);
+    try {
+      assertThat(tracer.getCurrentSpan()).isSameAs(span);
+      when(tracer.spanBuilderWithExplicitParent(same(SPAN_NAME), same(span)))
+          .thenReturn(spanBuilder);
+      assertThat(tracer.spanBuilder(SPAN_NAME)).isSameAs(spanBuilder);
+    } finally {
+      ws.close();
+    }
+  }
+
+  @Test
+  public void startSpanWithInvalidParentFromContext() {
+    Scope ws = tracer.withSpan(BlankSpan.INSTANCE);
+    try {
+      assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+      when(tracer.spanBuilderWithExplicitParent(same(SPAN_NAME), same(BlankSpan.INSTANCE)))
+          .thenReturn(spanBuilder);
+      assertThat(tracer.spanBuilder(SPAN_NAME)).isSameAs(spanBuilder);
+    } finally {
+      ws.close();
+    }
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TracestateTest.java b/api/src/test/java/io/opencensus/trace/TracestateTest.java
new file mode 100644
index 0000000..3374eb7
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TracestateTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.trace.Tracestate.Entry;
+import java.util.Arrays;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Tracestate}. */
+@RunWith(JUnit4.class)
+public class TracestateTest {
+  private static final String FIRST_KEY = "key_1";
+  private static final String SECOND_KEY = "key_2";
+  private static final String FIRST_VALUE = "value.1";
+  private static final String SECOND_VALUE = "value.2";
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private static final Tracestate EMPTY = Tracestate.builder().build();
+  private final Tracestate firstTracestate = EMPTY.toBuilder().set(FIRST_KEY, FIRST_VALUE).build();
+  private final Tracestate secondTracestate =
+      EMPTY.toBuilder().set(SECOND_KEY, SECOND_VALUE).build();
+  private final Tracestate multiValueTracestate =
+      EMPTY.toBuilder().set(FIRST_KEY, FIRST_VALUE).set(SECOND_KEY, SECOND_VALUE).build();
+
+  @Test
+  public void get() {
+    assertThat(firstTracestate.get(FIRST_KEY)).isEqualTo(FIRST_VALUE);
+    assertThat(secondTracestate.get(SECOND_KEY)).isEqualTo(SECOND_VALUE);
+    assertThat(multiValueTracestate.get(FIRST_KEY)).isEqualTo(FIRST_VALUE);
+    assertThat(multiValueTracestate.get(SECOND_KEY)).isEqualTo(SECOND_VALUE);
+  }
+
+  @Test
+  public void getEntries() {
+    assertThat(firstTracestate.getEntries()).containsExactly(Entry.create(FIRST_KEY, FIRST_VALUE));
+    assertThat(secondTracestate.getEntries())
+        .containsExactly(Entry.create(SECOND_KEY, SECOND_VALUE));
+    assertThat(multiValueTracestate.getEntries())
+        .containsExactly(
+            Entry.create(FIRST_KEY, FIRST_VALUE), Entry.create(SECOND_KEY, SECOND_VALUE));
+  }
+
+  @Test
+  public void disallowsNullKey() {
+    thrown.expect(NullPointerException.class);
+    EMPTY.toBuilder().set(null, FIRST_VALUE).build();
+  }
+
+  @Test
+  public void invalidFirstKeyCharacter() {
+    thrown.expect(IllegalArgumentException.class);
+    EMPTY.toBuilder().set("1_key", FIRST_VALUE).build();
+  }
+
+  @Test
+  public void invalidKeyCharacters() {
+    thrown.expect(IllegalArgumentException.class);
+    EMPTY.toBuilder().set("kEy_1", FIRST_VALUE).build();
+  }
+
+  @Test
+  public void invalidKeySize() {
+    char[] chars = new char[257];
+    Arrays.fill(chars, 'a');
+    String longKey = new String(chars);
+    thrown.expect(IllegalArgumentException.class);
+    EMPTY.toBuilder().set(longKey, FIRST_VALUE).build();
+  }
+
+  @Test
+  public void allAllowedKeyCharacters() {
+    StringBuilder stringBuilder = new StringBuilder();
+    for (char c = 'a'; c <= 'z'; c++) {
+      stringBuilder.append(c);
+    }
+    for (char c = '0'; c <= '9'; c++) {
+      stringBuilder.append(c);
+    }
+    stringBuilder.append('_');
+    stringBuilder.append('-');
+    stringBuilder.append('*');
+    stringBuilder.append('/');
+    String allowedKey = stringBuilder.toString();
+    assertThat(EMPTY.toBuilder().set(allowedKey, FIRST_VALUE).build().get(allowedKey))
+        .isEqualTo(FIRST_VALUE);
+  }
+
+  @Test
+  public void disallowsNullValue() {
+    thrown.expect(NullPointerException.class);
+    EMPTY.toBuilder().set(FIRST_KEY, null).build();
+  }
+
+  @Test
+  public void valueCannotContainEqual() {
+    thrown.expect(IllegalArgumentException.class);
+    EMPTY.toBuilder().set(FIRST_KEY, "my_vakue=5").build();
+  }
+
+  @Test
+  public void valueCannotContainComma() {
+    thrown.expect(IllegalArgumentException.class);
+    EMPTY.toBuilder().set(FIRST_KEY, "first,second").build();
+  }
+
+  @Test
+  public void valueCannotContainTrailingSpaces() {
+    thrown.expect(IllegalArgumentException.class);
+    EMPTY.toBuilder().set(FIRST_KEY, "first ").build();
+  }
+
+  @Test
+  public void invalidValueSize() {
+    char[] chars = new char[257];
+    Arrays.fill(chars, 'a');
+    String longValue = new String(chars);
+    thrown.expect(IllegalArgumentException.class);
+    EMPTY.toBuilder().set(FIRST_KEY, longValue).build();
+  }
+
+  @Test
+  public void allAllowedValueCharacters() {
+    StringBuilder stringBuilder = new StringBuilder();
+    for (char c = ' ' /* '\u0020' */; c <= '~' /* '\u007E' */; c++) {
+      if (c == ',' || c == '=') {
+        continue;
+      }
+      stringBuilder.append(c);
+    }
+    String allowedValue = stringBuilder.toString();
+    assertThat(EMPTY.toBuilder().set(FIRST_KEY, allowedValue).build().get(FIRST_KEY))
+        .isEqualTo(allowedValue);
+  }
+
+  @Test
+  public void addEntry() {
+    assertThat(firstTracestate.toBuilder().set(SECOND_KEY, SECOND_VALUE).build())
+        .isEqualTo(multiValueTracestate);
+  }
+
+  @Test
+  public void updateEntry() {
+    assertThat(firstTracestate.toBuilder().set(FIRST_KEY, SECOND_VALUE).build().get(FIRST_KEY))
+        .isEqualTo(SECOND_VALUE);
+    Tracestate updatedMultiValueTracestate =
+        multiValueTracestate.toBuilder().set(FIRST_KEY, SECOND_VALUE).build();
+    assertThat(updatedMultiValueTracestate.get(FIRST_KEY)).isEqualTo(SECOND_VALUE);
+    assertThat(updatedMultiValueTracestate.get(SECOND_KEY)).isEqualTo(SECOND_VALUE);
+  }
+
+  @Test
+  public void addAndUpdateEntry() {
+    assertThat(
+            firstTracestate
+                .toBuilder()
+                .set(FIRST_KEY, SECOND_VALUE) // update the existing entry
+                .set(SECOND_KEY, FIRST_VALUE) // add a new entry
+                .build()
+                .getEntries())
+        .containsExactly(
+            Entry.create(FIRST_KEY, SECOND_VALUE), Entry.create(SECOND_KEY, FIRST_VALUE));
+  }
+
+  @Test
+  public void addSameKey() {
+    assertThat(
+            EMPTY
+                .toBuilder()
+                .set(FIRST_KEY, SECOND_VALUE) // update the existing entry
+                .set(FIRST_KEY, FIRST_VALUE) // add a new entry
+                .build()
+                .getEntries())
+        .containsExactly(Entry.create(FIRST_KEY, FIRST_VALUE));
+  }
+
+  @Test
+  public void remove() {
+    assertThat(multiValueTracestate.toBuilder().remove(SECOND_KEY).build())
+        .isEqualTo(firstTracestate);
+  }
+
+  @Test
+  public void addAndRemoveEntry() {
+    assertThat(
+            EMPTY
+                .toBuilder()
+                .set(FIRST_KEY, SECOND_VALUE) // update the existing entry
+                .remove(FIRST_KEY) // add a new entry
+                .build())
+        .isEqualTo(EMPTY);
+  }
+
+  @Test
+  public void remove_NullNotAllowed() {
+    thrown.expect(NullPointerException.class);
+    multiValueTracestate.toBuilder().remove(null).build();
+  }
+
+  @Test
+  public void tracestate_EqualsAndHashCode() {
+    EqualsTester tester = new EqualsTester();
+    tester.addEqualityGroup(EMPTY, EMPTY);
+    tester.addEqualityGroup(firstTracestate, EMPTY.toBuilder().set(FIRST_KEY, FIRST_VALUE).build());
+    tester.addEqualityGroup(
+        secondTracestate, EMPTY.toBuilder().set(SECOND_KEY, SECOND_VALUE).build());
+    tester.testEquals();
+  }
+
+  @Test
+  public void tracestate_ToString() {
+    assertThat(EMPTY.toString()).isEqualTo("Tracestate{entries=[]}");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TracingTest.java b/api/src/test/java/io/opencensus/trace/TracingTest.java
new file mode 100644
index 0000000..e7c93a9
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TracingTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016-17, 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Tracing}. */
+@RunWith(JUnit4.class)
+public class TracingTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void loadTraceComponent_UsesProvidedClassLoader() {
+    final RuntimeException toThrow = new RuntimeException("UseClassLoader");
+    thrown.expect(RuntimeException.class);
+    thrown.expectMessage("UseClassLoader");
+    Tracing.loadTraceComponent(
+        new ClassLoader() {
+          @Override
+          public Class<?> loadClass(String name) {
+            throw toThrow;
+          }
+        });
+  }
+
+  @Test
+  public void loadTraceComponent_IgnoresMissingClasses() {
+    ClassLoader classLoader =
+        new ClassLoader() {
+          @Override
+          public Class<?> loadClass(String name) throws ClassNotFoundException {
+            throw new ClassNotFoundException();
+          }
+        };
+    assertThat(Tracing.loadTraceComponent(classLoader).getClass().getName())
+        .isEqualTo("io.opencensus.trace.TraceComponent$NoopTraceComponent");
+  }
+
+  @Test
+  public void defaultTracer() {
+    assertThat(Tracing.getTracer()).isSameAs(Tracer.getNoopTracer());
+  }
+
+  @Test
+  public void defaultBinaryPropagationHandler() {
+    assertThat(Tracing.getPropagationComponent())
+        .isSameAs(PropagationComponent.getNoopPropagationComponent());
+  }
+
+  @Test
+  public void defaultTraceExporter() {
+    assertThat(Tracing.getExportComponent())
+        .isInstanceOf(ExportComponent.newNoopExportComponent().getClass());
+  }
+
+  @Test
+  public void defaultTraceConfig() {
+    assertThat(Tracing.getTraceConfig()).isSameAs(TraceConfig.getNoopTraceConfig());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/config/TraceConfigTest.java b/api/src/test/java/io/opencensus/trace/config/TraceConfigTest.java
new file mode 100644
index 0000000..d48e089
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/config/TraceConfigTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.trace.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.samplers.Samplers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceConfig}. */
+@RunWith(JUnit4.class)
+public class TraceConfigTest {
+  TraceConfig traceConfig = TraceConfig.getNoopTraceConfig();
+
+  @Test
+  public void activeTraceParams_NoOpImplementation() {
+    assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+  }
+
+  @Test
+  public void updateActiveTraceParams_NoOpImplementation() {
+    TraceParams traceParams =
+        TraceParams.DEFAULT
+            .toBuilder()
+            .setSampler(Samplers.alwaysSample())
+            .setMaxNumberOfAttributes(8)
+            .setMaxNumberOfAnnotations(9)
+            .setMaxNumberOfNetworkEvents(10)
+            .setMaxNumberOfMessageEvents(10)
+            .setMaxNumberOfLinks(11)
+            .build();
+    traceConfig.updateActiveTraceParams(traceParams);
+    assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/config/TraceParamsTest.java b/api/src/test/java/io/opencensus/trace/config/TraceParamsTest.java
new file mode 100644
index 0000000..bdf07d5
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/config/TraceParamsTest.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.trace.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.samplers.Samplers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceParams}. */
+@RunWith(JUnit4.class)
+public class TraceParamsTest {
+  @Test
+  public void defaultTraceParams() {
+    assertThat(TraceParams.DEFAULT.getSampler()).isEqualTo(Samplers.probabilitySampler(1e-4));
+    assertThat(TraceParams.DEFAULT.getMaxNumberOfAttributes()).isEqualTo(32);
+    assertThat(TraceParams.DEFAULT.getMaxNumberOfAnnotations()).isEqualTo(32);
+    assertThat(TraceParams.DEFAULT.getMaxNumberOfNetworkEvents()).isEqualTo(128);
+    assertThat(TraceParams.DEFAULT.getMaxNumberOfMessageEvents()).isEqualTo(128);
+    assertThat(TraceParams.DEFAULT.getMaxNumberOfLinks()).isEqualTo(32);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void updateTraceParams_NullSampler() {
+    TraceParams.DEFAULT.toBuilder().setSampler(null).build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void updateTraceParams_NonPositiveMaxNumberOfAttributes() {
+    TraceParams.DEFAULT.toBuilder().setMaxNumberOfAttributes(0).build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void updateTraceParams_NonPositiveMaxNumberOfAnnotations() {
+    TraceParams.DEFAULT.toBuilder().setMaxNumberOfAnnotations(0).build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void updateTraceParams_NonPositiveMaxNumberOfNetworkEvents() {
+    TraceParams.DEFAULT.toBuilder().setMaxNumberOfNetworkEvents(0).build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void updateTraceParams_NonPositiveMaxNumberOfMessageEvents() {
+    TraceParams.DEFAULT.toBuilder().setMaxNumberOfMessageEvents(0).build();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void updateTraceParams_NonPositiveMaxNumberOfLinks() {
+    TraceParams.DEFAULT.toBuilder().setMaxNumberOfLinks(0).build();
+  }
+
+  @Test
+  public void updateTraceParams_All() {
+    TraceParams traceParams =
+        TraceParams.DEFAULT
+            .toBuilder()
+            .setSampler(Samplers.alwaysSample())
+            .setMaxNumberOfAttributes(8)
+            .setMaxNumberOfAnnotations(9)
+            .setMaxNumberOfMessageEvents(10)
+            .setMaxNumberOfLinks(11)
+            .build();
+    assertThat(traceParams.getSampler()).isEqualTo(Samplers.alwaysSample());
+    assertThat(traceParams.getMaxNumberOfAttributes()).isEqualTo(8);
+    assertThat(traceParams.getMaxNumberOfAnnotations()).isEqualTo(9);
+    // test maxNumberOfNetworkEvent can be set via maxNumberOfMessageEvent
+    assertThat(traceParams.getMaxNumberOfNetworkEvents()).isEqualTo(10);
+    assertThat(traceParams.getMaxNumberOfMessageEvents()).isEqualTo(10);
+    assertThat(traceParams.getMaxNumberOfLinks()).isEqualTo(11);
+  }
+
+  @Test
+  public void updateTraceParams_maxNumberOfNetworkEvents() {
+    TraceParams traceParams =
+        TraceParams.DEFAULT.toBuilder().setMaxNumberOfNetworkEvents(10).build();
+    assertThat(traceParams.getMaxNumberOfNetworkEvents()).isEqualTo(10);
+    // test maxNumberOfMessageEvent can be set via maxNumberOfNetworkEvent
+    assertThat(traceParams.getMaxNumberOfMessageEvents()).isEqualTo(10);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/export/ExportComponentTest.java b/api/src/test/java/io/opencensus/trace/export/ExportComponentTest.java
new file mode 100644
index 0000000..d7f385d
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/export/ExportComponentTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ExportComponent}. */
+@RunWith(JUnit4.class)
+public class ExportComponentTest {
+  private final ExportComponent exportComponent = ExportComponent.newNoopExportComponent();
+
+  @Test
+  public void implementationOfSpanExporter() {
+    assertThat(exportComponent.getSpanExporter()).isEqualTo(SpanExporter.getNoopSpanExporter());
+  }
+
+  @Test
+  public void implementationOfActiveSpans() {
+    assertThat(exportComponent.getRunningSpanStore())
+        .isEqualTo(RunningSpanStore.getNoopRunningSpanStore());
+  }
+
+  @Test
+  public void implementationOfSampledSpanStore() {
+    assertThat(exportComponent.getSampledSpanStore())
+        .isInstanceOf(SampledSpanStore.newNoopSampledSpanStore().getClass());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/export/NoopRunningSpanStoreTest.java b/api/src/test/java/io/opencensus/trace/export/NoopRunningSpanStoreTest.java
new file mode 100644
index 0000000..960da27
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/export/NoopRunningSpanStoreTest.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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Collection;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link NoopRunningSpanStore}. */
+@RunWith(JUnit4.class)
+public final class NoopRunningSpanStoreTest {
+
+  private final RunningSpanStore runningSpanStore =
+      ExportComponent.newNoopExportComponent().getRunningSpanStore();
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void noopRunningSpanStore_GetSummary() {
+    RunningSpanStore.Summary summary = runningSpanStore.getSummary();
+    assertThat(summary.getPerSpanNameSummary()).isEmpty();
+  }
+
+  @Test
+  public void noopRunningSpanStore_GetRunningSpans_DisallowsNull() {
+    thrown.expect(NullPointerException.class);
+    runningSpanStore.getRunningSpans(null);
+  }
+
+  @Test
+  public void noopRunningSpanStore_GetRunningSpans() {
+    Collection<SpanData> runningSpans =
+        runningSpanStore.getRunningSpans(RunningSpanStore.Filter.create("TestSpan", 0));
+    assertThat(runningSpans).isEmpty();
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/export/NoopSampledSpanStoreTest.java b/api/src/test/java/io/opencensus/trace/export/NoopSampledSpanStoreTest.java
new file mode 100644
index 0000000..6e9c7b0
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/export/NoopSampledSpanStoreTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.opencensus.trace.Status.CanonicalCode;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link NoopSampledSpanStore}. */
+@RunWith(JUnit4.class)
+public final class NoopSampledSpanStoreTest {
+
+  private static final SampledSpanStore.PerSpanNameSummary EMPTY_PER_SPAN_NAME_SUMMARY =
+      SampledSpanStore.PerSpanNameSummary.create(
+          Collections.<SampledSpanStore.LatencyBucketBoundaries, Integer>emptyMap(),
+          Collections.<CanonicalCode, Integer>emptyMap());
+
+  @Test
+  public void noopSampledSpanStore_RegisterUnregisterAndGetSummary() {
+    // should return empty before register
+    SampledSpanStore sampledSpanStore =
+        ExportComponent.newNoopExportComponent().getSampledSpanStore();
+    SampledSpanStore.Summary summary = sampledSpanStore.getSummary();
+    assertThat(summary.getPerSpanNameSummary()).isEmpty();
+
+    // should return non-empty summaries with zero latency/error sampled spans after register
+    sampledSpanStore.registerSpanNamesForCollection(
+        Collections.unmodifiableList(Lists.newArrayList("TestSpan1", "TestSpan2", "TestSpan3")));
+    summary = sampledSpanStore.getSummary();
+    assertThat(summary.getPerSpanNameSummary())
+        .containsExactly(
+            "TestSpan1", EMPTY_PER_SPAN_NAME_SUMMARY,
+            "TestSpan2", EMPTY_PER_SPAN_NAME_SUMMARY,
+            "TestSpan3", EMPTY_PER_SPAN_NAME_SUMMARY);
+
+    // should unregister specific spanNames
+    sampledSpanStore.unregisterSpanNamesForCollection(
+        Collections.unmodifiableList(Lists.newArrayList("TestSpan1", "TestSpan3")));
+    summary = sampledSpanStore.getSummary();
+    assertThat(summary.getPerSpanNameSummary())
+        .containsExactly("TestSpan2", EMPTY_PER_SPAN_NAME_SUMMARY);
+  }
+
+  @Test
+  public void noopSampledSpanStore_GetLatencySampledSpans() {
+    SampledSpanStore sampledSpanStore =
+        ExportComponent.newNoopExportComponent().getSampledSpanStore();
+    Collection<SpanData> latencySampledSpans =
+        sampledSpanStore.getLatencySampledSpans(
+            SampledSpanStore.LatencyFilter.create("TestLatencyFilter", 0, 0, 0));
+    assertThat(latencySampledSpans).isEmpty();
+  }
+
+  @Test
+  public void noopSampledSpanStore_GetErrorSampledSpans() {
+    SampledSpanStore sampledSpanStore =
+        ExportComponent.newNoopExportComponent().getSampledSpanStore();
+    Collection<SpanData> errorSampledSpans =
+        sampledSpanStore.getErrorSampledSpans(
+            SampledSpanStore.ErrorFilter.create("TestErrorFilter", null, 0));
+    assertThat(errorSampledSpans).isEmpty();
+  }
+
+  @Test
+  public void noopSampledSpanStore_GetRegisteredSpanNamesForCollection() {
+    SampledSpanStore sampledSpanStore =
+        ExportComponent.newNoopExportComponent().getSampledSpanStore();
+    sampledSpanStore.registerSpanNamesForCollection(
+        Collections.unmodifiableList(Lists.newArrayList("TestSpan3", "TestSpan4")));
+    Set<String> registeredSpanNames = sampledSpanStore.getRegisteredSpanNamesForCollection();
+    assertThat(registeredSpanNames).containsExactly("TestSpan3", "TestSpan4");
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/export/SpanDataTest.java b/api/src/test/java/io/opencensus/trace/export/SpanDataTest.java
new file mode 100644
index 0000000..b991d14
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/export/SpanDataTest.java
@@ -0,0 +1,321 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Link.Type;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.NetworkEvent;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData.Attributes;
+import io.opencensus.trace.export.SpanData.Links;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SpanData}. */
+@RunWith(JUnit4.class)
+public class SpanDataTest {
+  private static final Timestamp startTimestamp = Timestamp.create(123, 456);
+  private static final Timestamp eventTimestamp1 = Timestamp.create(123, 457);
+  private static final Timestamp eventTimestamp2 = Timestamp.create(123, 458);
+  private static final Timestamp eventTimestamp3 = Timestamp.create(123, 459);
+  private static final Timestamp endTimestamp = Timestamp.create(123, 460);
+  private static final String SPAN_NAME = "MySpanName";
+  private static final String ANNOTATION_TEXT = "MyAnnotationText";
+  private static final Annotation annotation = Annotation.fromDescription(ANNOTATION_TEXT);
+  private static final NetworkEvent recvNetworkEvent =
+      NetworkEvent.builder(NetworkEvent.Type.RECV, 1).build();
+  private static final NetworkEvent sentNetworkEvent =
+      NetworkEvent.builder(NetworkEvent.Type.SENT, 1).build();
+  private static final MessageEvent recvMessageEvent =
+      MessageEvent.builder(MessageEvent.Type.RECEIVED, 1).build();
+  private static final MessageEvent sentMessageEvent =
+      MessageEvent.builder(MessageEvent.Type.SENT, 1).build();
+  private static final Status status = Status.DEADLINE_EXCEEDED.withDescription("TooSlow");
+  private static final int CHILD_SPAN_COUNT = 13;
+  private final Random random = new Random(1234);
+  private final SpanContext spanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+  private final SpanId parentSpanId = SpanId.generateRandomId(random);
+  private final Map<String, AttributeValue> attributesMap = new HashMap<String, AttributeValue>();
+  private final List<TimedEvent<Annotation>> annotationsList =
+      new ArrayList<TimedEvent<Annotation>>();
+  private final List<TimedEvent<NetworkEvent>> networkEventsList =
+      new ArrayList<SpanData.TimedEvent<NetworkEvent>>();
+  private final List<TimedEvent<MessageEvent>> messageEventsList =
+      new ArrayList<SpanData.TimedEvent<MessageEvent>>();
+  private final List<Link> linksList = new ArrayList<Link>();
+
+  private Attributes attributes;
+  private TimedEvents<Annotation> annotations;
+  private TimedEvents<NetworkEvent> networkEvents;
+  private TimedEvents<MessageEvent> messageEvents;
+  private Links links;
+
+  @Before
+  public void setUp() {
+    attributesMap.put("MyAttributeKey1", AttributeValue.longAttributeValue(10));
+    attributesMap.put("MyAttributeKey2", AttributeValue.booleanAttributeValue(true));
+    attributes = Attributes.create(attributesMap, 1);
+    annotationsList.add(SpanData.TimedEvent.create(eventTimestamp1, annotation));
+    annotationsList.add(SpanData.TimedEvent.create(eventTimestamp3, annotation));
+    annotations = TimedEvents.create(annotationsList, 2);
+    networkEventsList.add(SpanData.TimedEvent.create(eventTimestamp1, recvNetworkEvent));
+    networkEventsList.add(SpanData.TimedEvent.create(eventTimestamp2, sentNetworkEvent));
+    networkEvents = TimedEvents.create(networkEventsList, 3);
+    messageEventsList.add(SpanData.TimedEvent.create(eventTimestamp1, recvMessageEvent));
+    messageEventsList.add(SpanData.TimedEvent.create(eventTimestamp2, sentMessageEvent));
+    messageEvents = TimedEvents.create(messageEventsList, 3);
+    linksList.add(Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN));
+    links = Links.create(linksList, 0);
+  }
+
+  @Test
+  public void spanData_AllValues() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            true,
+            SPAN_NAME,
+            Kind.SERVER,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    assertThat(spanData.getContext()).isEqualTo(spanContext);
+    assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+    assertThat(spanData.getHasRemoteParent()).isTrue();
+    assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+    assertThat(spanData.getKind()).isEqualTo(Kind.SERVER);
+    assertThat(spanData.getStartTimestamp()).isEqualTo(startTimestamp);
+    assertThat(spanData.getAttributes()).isEqualTo(attributes);
+    assertThat(spanData.getAnnotations()).isEqualTo(annotations);
+    assertThat(spanData.getNetworkEvents()).isEqualTo(networkEvents);
+    assertThat(spanData.getMessageEvents()).isEqualTo(messageEvents);
+    assertThat(spanData.getLinks()).isEqualTo(links);
+    assertThat(spanData.getChildSpanCount()).isEqualTo(CHILD_SPAN_COUNT);
+    assertThat(spanData.getStatus()).isEqualTo(status);
+    assertThat(spanData.getEndTimestamp()).isEqualTo(endTimestamp);
+  }
+
+  @Test
+  public void spanData_Create_Compatibility() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            true,
+            SPAN_NAME,
+            null,
+            startTimestamp,
+            attributes,
+            annotations,
+            networkEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    assertThat(spanData.getContext()).isEqualTo(spanContext);
+    assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+    assertThat(spanData.getHasRemoteParent()).isTrue();
+    assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+    assertThat(spanData.getStartTimestamp()).isEqualTo(startTimestamp);
+    assertThat(spanData.getAttributes()).isEqualTo(attributes);
+    assertThat(spanData.getAnnotations()).isEqualTo(annotations);
+    assertThat(spanData.getNetworkEvents()).isEqualTo(networkEvents);
+    assertThat(spanData.getMessageEvents()).isEqualTo(messageEvents);
+    assertThat(spanData.getLinks()).isEqualTo(links);
+    assertThat(spanData.getChildSpanCount()).isEqualTo(CHILD_SPAN_COUNT);
+    assertThat(spanData.getStatus()).isEqualTo(status);
+    assertThat(spanData.getEndTimestamp()).isEqualTo(endTimestamp);
+  }
+
+  @Test
+  public void spanData_RootActiveSpan() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            null,
+            null,
+            SPAN_NAME,
+            null,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            null,
+            null,
+            null);
+    assertThat(spanData.getContext()).isEqualTo(spanContext);
+    assertThat(spanData.getParentSpanId()).isNull();
+    assertThat(spanData.getHasRemoteParent()).isNull();
+    assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+    assertThat(spanData.getStartTimestamp()).isEqualTo(startTimestamp);
+    assertThat(spanData.getAttributes()).isEqualTo(attributes);
+    assertThat(spanData.getAnnotations()).isEqualTo(annotations);
+    assertThat(spanData.getNetworkEvents()).isEqualTo(networkEvents);
+    assertThat(spanData.getMessageEvents()).isEqualTo(messageEvents);
+    assertThat(spanData.getLinks()).isEqualTo(links);
+    assertThat(spanData.getChildSpanCount()).isNull();
+    assertThat(spanData.getStatus()).isNull();
+    assertThat(spanData.getEndTimestamp()).isNull();
+  }
+
+  @Test
+  public void spanData_AllDataEmpty() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            false,
+            SPAN_NAME,
+            null,
+            startTimestamp,
+            Attributes.create(Collections.<String, AttributeValue>emptyMap(), 0),
+            TimedEvents.create(Collections.<SpanData.TimedEvent<Annotation>>emptyList(), 0),
+            TimedEvents.create(Collections.<SpanData.TimedEvent<MessageEvent>>emptyList(), 0),
+            Links.create(Collections.<Link>emptyList(), 0),
+            0,
+            status,
+            endTimestamp);
+    assertThat(spanData.getContext()).isEqualTo(spanContext);
+    assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+    assertThat(spanData.getHasRemoteParent()).isFalse();
+    assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+    assertThat(spanData.getStartTimestamp()).isEqualTo(startTimestamp);
+    assertThat(spanData.getAttributes().getAttributeMap().isEmpty()).isTrue();
+    assertThat(spanData.getAnnotations().getEvents().isEmpty()).isTrue();
+    assertThat(spanData.getNetworkEvents().getEvents().isEmpty()).isTrue();
+    assertThat(spanData.getMessageEvents().getEvents().isEmpty()).isTrue();
+    assertThat(spanData.getLinks().getLinks().isEmpty()).isTrue();
+    assertThat(spanData.getChildSpanCount()).isEqualTo(0);
+    assertThat(spanData.getStatus()).isEqualTo(status);
+    assertThat(spanData.getEndTimestamp()).isEqualTo(endTimestamp);
+  }
+
+  @Test
+  public void spanDataEquals() {
+    SpanData allSpanData1 =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            false,
+            SPAN_NAME,
+            Kind.CLIENT,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    SpanData allSpanData2 =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            false,
+            SPAN_NAME,
+            Kind.CLIENT,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    SpanData emptySpanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            false,
+            SPAN_NAME,
+            null,
+            startTimestamp,
+            Attributes.create(Collections.<String, AttributeValue>emptyMap(), 0),
+            TimedEvents.create(Collections.<SpanData.TimedEvent<Annotation>>emptyList(), 0),
+            TimedEvents.create(Collections.<SpanData.TimedEvent<MessageEvent>>emptyList(), 0),
+            Links.create(Collections.<Link>emptyList(), 0),
+            0,
+            status,
+            endTimestamp);
+    new EqualsTester()
+        .addEqualityGroup(allSpanData1, allSpanData2)
+        .addEqualityGroup(emptySpanData)
+        .testEquals();
+  }
+
+  @Test
+  public void spanData_ToString() {
+    String spanDataString =
+        SpanData.create(
+                spanContext,
+                parentSpanId,
+                false,
+                SPAN_NAME,
+                Kind.CLIENT,
+                startTimestamp,
+                attributes,
+                annotations,
+                messageEvents,
+                links,
+                CHILD_SPAN_COUNT,
+                status,
+                endTimestamp)
+            .toString();
+    assertThat(spanDataString).contains(spanContext.toString());
+    assertThat(spanDataString).contains(parentSpanId.toString());
+    assertThat(spanDataString).contains(SPAN_NAME);
+    assertThat(spanDataString).contains(Kind.CLIENT.toString());
+    assertThat(spanDataString).contains(startTimestamp.toString());
+    assertThat(spanDataString).contains(attributes.toString());
+    assertThat(spanDataString).contains(annotations.toString());
+    assertThat(spanDataString).contains(messageEvents.toString());
+    assertThat(spanDataString).contains(links.toString());
+    assertThat(spanDataString).contains(status.toString());
+    assertThat(spanDataString).contains(endTimestamp.toString());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/internal/BaseMessageEventUtilsTest.java b/api/src/test/java/io/opencensus/trace/internal/BaseMessageEventUtilsTest.java
new file mode 100644
index 0000000..4f8c850
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/internal/BaseMessageEventUtilsTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018, 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.trace.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.NetworkEvent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BaseMessageEventUtils}. */
+@RunWith(JUnit4.class)
+public class BaseMessageEventUtilsTest {
+  private static final long SENT_EVENT_ID = 12345L;
+  private static final long RECV_EVENT_ID = 67890L;
+  private static final long UNCOMPRESSED_SIZE = 100;
+  private static final long COMPRESSED_SIZE = 99;
+
+  private static final MessageEvent SENT_MESSAGE_EVENT =
+      MessageEvent.builder(MessageEvent.Type.SENT, SENT_EVENT_ID)
+          .setUncompressedMessageSize(UNCOMPRESSED_SIZE)
+          .setCompressedMessageSize(COMPRESSED_SIZE)
+          .build();
+  private static final MessageEvent RECV_MESSAGE_EVENT =
+      MessageEvent.builder(MessageEvent.Type.RECEIVED, RECV_EVENT_ID)
+          .setUncompressedMessageSize(UNCOMPRESSED_SIZE)
+          .setCompressedMessageSize(COMPRESSED_SIZE)
+          .build();
+  private static final NetworkEvent SENT_NETWORK_EVENT =
+      NetworkEvent.builder(NetworkEvent.Type.SENT, SENT_EVENT_ID)
+          .setUncompressedMessageSize(UNCOMPRESSED_SIZE)
+          .setCompressedMessageSize(COMPRESSED_SIZE)
+          .build();
+  private static final NetworkEvent RECV_NETWORK_EVENT =
+      NetworkEvent.builder(NetworkEvent.Type.RECV, RECV_EVENT_ID)
+          .setUncompressedMessageSize(UNCOMPRESSED_SIZE)
+          .setCompressedMessageSize(COMPRESSED_SIZE)
+          .build();
+
+  @Test
+  public void networkEventToMessageEvent() {
+    assertThat(BaseMessageEventUtils.asMessageEvent(SENT_NETWORK_EVENT))
+        .isEqualTo(SENT_MESSAGE_EVENT);
+    assertThat(BaseMessageEventUtils.asMessageEvent(RECV_NETWORK_EVENT))
+        .isEqualTo(RECV_MESSAGE_EVENT);
+  }
+
+  @Test
+  public void messageEventToNetworkEvent() {
+    assertThat(BaseMessageEventUtils.asNetworkEvent(SENT_MESSAGE_EVENT))
+        .isEqualTo(SENT_NETWORK_EVENT);
+    assertThat(BaseMessageEventUtils.asNetworkEvent(RECV_MESSAGE_EVENT))
+        .isEqualTo(RECV_NETWORK_EVENT);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/propagation/BinaryFormatTest.java b/api/src/test/java/io/opencensus/trace/propagation/BinaryFormatTest.java
new file mode 100644
index 0000000..64544ff
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/propagation/BinaryFormatTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.SpanContext;
+import java.text.ParseException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BinaryFormat}. */
+@RunWith(JUnit4.class)
+public class BinaryFormatTest {
+  private static final BinaryFormat binaryFormat = BinaryFormat.getNoopBinaryFormat();
+
+  @Test(expected = NullPointerException.class)
+  public void toBinaryValue_NullSpanContext() {
+    binaryFormat.toBinaryValue(null);
+  }
+
+  @Test
+  public void toBinaryValue_NotNullSpanContext() {
+    assertThat(binaryFormat.toBinaryValue(SpanContext.INVALID)).isEqualTo(new byte[0]);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void toByteArray_NullSpanContext() {
+    binaryFormat.toByteArray(null);
+  }
+
+  @Test
+  public void toByteArray_NotNullSpanContext() {
+    assertThat(binaryFormat.toByteArray(SpanContext.INVALID)).isEqualTo(new byte[0]);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void fromBinaryValue_NullInput() throws ParseException {
+    binaryFormat.fromBinaryValue(null);
+  }
+
+  @Test
+  public void fromBinaryValue_NotNullInput() throws ParseException {
+    assertThat(binaryFormat.fromBinaryValue(new byte[0])).isEqualTo(SpanContext.INVALID);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void fromByteArray_NullInput() throws SpanContextParseException {
+    binaryFormat.fromByteArray(null);
+  }
+
+  @Test
+  public void fromByteArray_NotNullInput() throws SpanContextParseException {
+    assertThat(binaryFormat.fromByteArray(new byte[0])).isEqualTo(SpanContext.INVALID);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/propagation/PropagationComponentTest.java b/api/src/test/java/io/opencensus/trace/propagation/PropagationComponentTest.java
new file mode 100644
index 0000000..ba64e98
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/propagation/PropagationComponentTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PropagationComponent}. */
+@RunWith(JUnit4.class)
+public class PropagationComponentTest {
+  private final PropagationComponent propagationComponent =
+      PropagationComponent.getNoopPropagationComponent();
+
+  @Test
+  public void implementationOfBinaryFormat() {
+    assertThat(propagationComponent.getBinaryFormat())
+        .isEqualTo(BinaryFormat.getNoopBinaryFormat());
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/propagation/SpanContextParseExceptionTest.java b/api/src/test/java/io/opencensus/trace/propagation/SpanContextParseExceptionTest.java
new file mode 100644
index 0000000..92efb35
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/propagation/SpanContextParseExceptionTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SpanContextParseException}. */
+@RunWith(JUnit4.class)
+public class SpanContextParseExceptionTest {
+
+  @Test
+  public void createWithMessage() {
+    assertThat(new SpanContextParseException("my message").getMessage()).isEqualTo("my message");
+  }
+
+  @Test
+  public void createWithMessageAndCause() {
+    IOException cause = new IOException();
+    SpanContextParseException parseException = new SpanContextParseException("my message", cause);
+    assertThat(parseException.getMessage()).isEqualTo("my message");
+    assertThat(parseException.getCause()).isEqualTo(cause);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/propagation/TextFormatTest.java b/api/src/test/java/io/opencensus/trace/propagation/TextFormatTest.java
new file mode 100644
index 0000000..c2e6e12
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/propagation/TextFormatTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TextFormat}. */
+@RunWith(JUnit4.class)
+public class TextFormatTest {
+  private static final TextFormat textFormat = TextFormat.getNoopTextFormat();
+
+  @Test(expected = NullPointerException.class)
+  public void inject_NullSpanContext() {
+    textFormat.inject(
+        null,
+        new Object(),
+        new Setter<Object>() {
+          @Override
+          public void put(Object carrier, String key, String value) {}
+        });
+  }
+
+  @Test
+  public void inject_NotNullSpanContext_DoesNotFail() {
+    textFormat.inject(
+        SpanContext.INVALID,
+        new Object(),
+        new Setter<Object>() {
+          @Override
+          public void put(Object carrier, String key, String value) {}
+        });
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void fromHeaders_NullGetter() throws SpanContextParseException {
+    textFormat.extract(new Object(), null);
+  }
+
+  @Test
+  public void fromHeaders_NotNullGetter() throws SpanContextParseException {
+    assertThat(
+            textFormat.extract(
+                new Object(),
+                new Getter<Object>() {
+                  @Nullable
+                  @Override
+                  public String get(Object carrier, String key) {
+                    return null;
+                  }
+                }))
+        .isSameAs(SpanContext.INVALID);
+  }
+}
diff --git a/api/src/test/java/io/opencensus/trace/samplers/SamplersTest.java b/api/src/test/java/io/opencensus/trace/samplers/SamplersTest.java
new file mode 100644
index 0000000..7a46e97
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/samplers/SamplersTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.trace.samplers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.NoopSpan;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Random;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Samplers}. */
+@RunWith(JUnit4.class)
+public class SamplersTest {
+  private static final String SPAN_NAME = "MySpanName";
+  private static final int NUM_SAMPLE_TRIES = 1000;
+  private final Random random = new Random(1234);
+  private final TraceId traceId = TraceId.generateRandomId(random);
+  private final SpanId parentSpanId = SpanId.generateRandomId(random);
+  private final SpanId spanId = SpanId.generateRandomId(random);
+  private final SpanContext sampledSpanContext =
+      SpanContext.create(traceId, parentSpanId, TraceOptions.builder().setIsSampled(true).build());
+  private final SpanContext notSampledSpanContext =
+      SpanContext.create(traceId, parentSpanId, TraceOptions.DEFAULT);
+  private final Span sampledSpan =
+      new NoopSpan(sampledSpanContext, EnumSet.of(Span.Options.RECORD_EVENTS));
+
+  @Test
+  public void alwaysSampleSampler_AlwaysReturnTrue() {
+    // Sampled parent.
+    assertThat(
+            Samplers.alwaysSample()
+                .shouldSample(
+                    sampledSpanContext,
+                    false,
+                    traceId,
+                    spanId,
+                    "Another name",
+                    Collections.<Span>emptyList()))
+        .isTrue();
+    // Not sampled parent.
+    assertThat(
+            Samplers.alwaysSample()
+                .shouldSample(
+                    notSampledSpanContext,
+                    false,
+                    traceId,
+                    spanId,
+                    "Yet another name",
+                    Collections.<Span>emptyList()))
+        .isTrue();
+  }
+
+  @Test
+  public void alwaysSampleSampler_ToString() {
+    assertThat(Samplers.alwaysSample().toString()).isEqualTo("AlwaysSampleSampler");
+  }
+
+  @Test
+  public void neverSampleSampler_AlwaysReturnFalse() {
+    // Sampled parent.
+    assertThat(
+            Samplers.neverSample()
+                .shouldSample(
+                    sampledSpanContext,
+                    false,
+                    traceId,
+                    spanId,
+                    "bar",
+                    Collections.<Span>emptyList()))
+        .isFalse();
+    // Not sampled parent.
+    assertThat(
+            Samplers.neverSample()
+                .shouldSample(
+                    notSampledSpanContext,
+                    false,
+                    traceId,
+                    spanId,
+                    "quux",
+                    Collections.<Span>emptyList()))
+        .isFalse();
+  }
+
+  @Test
+  public void neverSampleSampler_ToString() {
+    assertThat(Samplers.neverSample().toString()).isEqualTo("NeverSampleSampler");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void probabilitySampler_outOfRangeHighProbability() {
+    Samplers.probabilitySampler(1.01);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void probabilitySampler_outOfRangeLowProbability() {
+    Samplers.probabilitySampler(-0.00001);
+  }
+
+  // Applies the given sampler to NUM_SAMPLE_TRIES random traceId/spanId pairs.
+  private static void assertSamplerSamplesWithProbability(
+      Sampler sampler, SpanContext parent, List<Span> parentLinks, double probability) {
+    Random random = new Random(1234);
+    int count = 0; // Count of spans with sampling enabled
+    for (int i = 0; i < NUM_SAMPLE_TRIES; i++) {
+      if (sampler.shouldSample(
+          parent,
+          false,
+          TraceId.generateRandomId(random),
+          SpanId.generateRandomId(random),
+          SPAN_NAME,
+          parentLinks)) {
+        count++;
+      }
+    }
+    double proportionSampled = (double) count / NUM_SAMPLE_TRIES;
+    // Allow for a large amount of slop (+/- 10%) in number of sampled traces, to avoid flakiness.
+    assertThat(proportionSampled < probability + 0.1 && proportionSampled > probability - 0.1)
+        .isTrue();
+  }
+
+  @Test
+  public void probabilitySampler_DifferentProbabilities_NotSampledParent() {
+    final Sampler neverSample = Samplers.probabilitySampler(0.0);
+    assertSamplerSamplesWithProbability(
+        neverSample, notSampledSpanContext, Collections.<Span>emptyList(), 0.0);
+    final Sampler alwaysSample = Samplers.probabilitySampler(1.0);
+    assertSamplerSamplesWithProbability(
+        alwaysSample, notSampledSpanContext, Collections.<Span>emptyList(), 1.0);
+    final Sampler fiftyPercentSample = Samplers.probabilitySampler(0.5);
+    assertSamplerSamplesWithProbability(
+        fiftyPercentSample, notSampledSpanContext, Collections.<Span>emptyList(), 0.5);
+    final Sampler twentyPercentSample = Samplers.probabilitySampler(0.2);
+    assertSamplerSamplesWithProbability(
+        twentyPercentSample, notSampledSpanContext, Collections.<Span>emptyList(), 0.2);
+    final Sampler twoThirdsSample = Samplers.probabilitySampler(2.0 / 3.0);
+    assertSamplerSamplesWithProbability(
+        twoThirdsSample, notSampledSpanContext, Collections.<Span>emptyList(), 2.0 / 3.0);
+  }
+
+  @Test
+  public void probabilitySampler_DifferentProbabilities_SampledParent() {
+    final Sampler neverSample = Samplers.probabilitySampler(0.0);
+    assertSamplerSamplesWithProbability(
+        neverSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+    final Sampler alwaysSample = Samplers.probabilitySampler(1.0);
+    assertSamplerSamplesWithProbability(
+        alwaysSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+    final Sampler fiftyPercentSample = Samplers.probabilitySampler(0.5);
+    assertSamplerSamplesWithProbability(
+        fiftyPercentSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+    final Sampler twentyPercentSample = Samplers.probabilitySampler(0.2);
+    assertSamplerSamplesWithProbability(
+        twentyPercentSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+    final Sampler twoThirdsSample = Samplers.probabilitySampler(2.0 / 3.0);
+    assertSamplerSamplesWithProbability(
+        twoThirdsSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+  }
+
+  @Test
+  public void probabilitySampler_DifferentProbabilities_SampledParentLink() {
+    final Sampler neverSample = Samplers.probabilitySampler(0.0);
+    assertSamplerSamplesWithProbability(
+        neverSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+    final Sampler alwaysSample = Samplers.probabilitySampler(1.0);
+    assertSamplerSamplesWithProbability(
+        alwaysSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+    final Sampler fiftyPercentSample = Samplers.probabilitySampler(0.5);
+    assertSamplerSamplesWithProbability(
+        fiftyPercentSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+    final Sampler twentyPercentSample = Samplers.probabilitySampler(0.2);
+    assertSamplerSamplesWithProbability(
+        twentyPercentSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+    final Sampler twoThirdsSample = Samplers.probabilitySampler(2.0 / 3.0);
+    assertSamplerSamplesWithProbability(
+        twoThirdsSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+  }
+
+  @Test
+  public void probabilitySampler_SampleBasedOnTraceId() {
+    final Sampler defaultProbability = Samplers.probabilitySampler(0.0001);
+    // This traceId will not be sampled by the ProbabilitySampler because the first 8 bytes as long
+    // is not less than probability * Long.MAX_VALUE;
+    TraceId notSampledtraceId =
+        TraceId.fromBytes(
+            new byte[] {
+              (byte) 0x8F,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              0,
+              0,
+              0,
+              0,
+              0,
+              0,
+              0,
+              0
+            });
+    assertThat(
+            defaultProbability.shouldSample(
+                null,
+                false,
+                notSampledtraceId,
+                SpanId.generateRandomId(random),
+                SPAN_NAME,
+                Collections.<Span>emptyList()))
+        .isFalse();
+    // This traceId will be sampled by the ProbabilitySampler because the first 8 bytes as long
+    // is less than probability * Long.MAX_VALUE;
+    TraceId sampledtraceId =
+        TraceId.fromBytes(
+            new byte[] {
+              (byte) 0x00,
+              (byte) 0x00,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              0,
+              0,
+              0,
+              0,
+              0,
+              0,
+              0,
+              0
+            });
+    assertThat(
+            defaultProbability.shouldSample(
+                null,
+                false,
+                sampledtraceId,
+                SpanId.generateRandomId(random),
+                SPAN_NAME,
+                Collections.<Span>emptyList()))
+        .isTrue();
+  }
+
+  @Test
+  public void probabilitySampler_getDescription() {
+    assertThat((Samplers.probabilitySampler(0.5)).getDescription())
+        .isEqualTo(String.format("ProbabilitySampler{%.6f}", 0.5));
+  }
+
+  @Test
+  public void probabilitySampler_ToString() {
+    assertThat((Samplers.probabilitySampler(0.5)).toString()).contains("0.5");
+  }
+}
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..34493a9
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,10 @@
+install:
+  - git submodule update --init --recursive
+
+build_script:
+  # The Gradle build script runs the integration tests of contrib/agent using different Java
+  # versions. %JAVA_HOMES% lists the home directories of the JDK installations used for
+  # integration testing. Also see https://www.appveyor.com/docs/build-environment/#java.
+  - set JAVA_HOMES=C:\Program Files\Java\jdk1.6.0\jre;C:\Program Files\Java\jdk1.7.0\jre;C:\Program Files\Java\jdk1.8.0\jre
+  - gradlew.bat clean assemble check --stacktrace
+  - pushd examples && gradlew.bat clean assemble check --stacktrace && popd
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 0000000..e591a8d
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,3 @@
+# OpenCensus Benchmarks
+
+See [here](../CONTRIBUTING.md#benchmarks) for how to run and debug issues with benchmarks.
\ No newline at end of file
diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle
new file mode 100644
index 0000000..04688dd
--- /dev/null
+++ b/benchmarks/build.gradle
@@ -0,0 +1,18 @@
+description = 'OpenCensus Benchmarks'
+
+dependencies {
+    compile project(':opencensus-api'),
+            project(':opencensus-impl-core'),
+            project(':opencensus-impl-lite'),
+            project(':opencensus-impl')
+}
+
+jmhReport {
+    jmhResultPath = project.file("${project.buildDir}/reports/jmh/results.json")
+    jmhReportOutput = project.file("${project.buildDir}/reports/jmh")
+}
+
+tasks.jmh.finalizedBy tasks.jmhReport
+
+// Disable checkstyle if not java8.
+checkstyleJmh.enabled = JavaVersion.current().isJava8Compatible()
diff --git a/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/BenchmarksUtil.java b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/BenchmarksUtil.java
new file mode 100644
index 0000000..e917817
--- /dev/null
+++ b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/BenchmarksUtil.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018, 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.benchmarks.trace;
+
+import io.opencensus.impllite.trace.TraceComponentImplLite;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+
+/** Util class for Benchmarks. */
+final class BenchmarksUtil {
+  private static final TraceComponentImplLite traceComponentImplLite = new TraceComponentImplLite();
+
+  static Tracer getTracer(String implementation) {
+    if (implementation.equals("impl")) {
+      // We can return the global tracer here because if impl is linked the global tracer will be
+      // the impl one.
+      // TODO(bdrutu): Make everything not be a singleton (disruptor, etc.) and use a new
+      // TraceComponentImpl similar to TraceComponentImplLite.
+      return Tracing.getTracer();
+    } else if (implementation.equals("impl-lite")) {
+      return traceComponentImplLite.getTracer();
+    } else {
+      throw new RuntimeException("Invalid tracer implementation requested.");
+    }
+  }
+
+  // Avoid instances of this class.
+  private BenchmarksUtil() {}
+}
diff --git a/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/RecordTraceEventsBenchmark.java b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/RecordTraceEventsBenchmark.java
new file mode 100644
index 0000000..992937a
--- /dev/null
+++ b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/RecordTraceEventsBenchmark.java
@@ -0,0 +1,122 @@
+/*
+ * 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.benchmarks.trace;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+
+/** Benchmarks for {@link Span} to record trace events. */
+@State(Scope.Benchmark)
+public class RecordTraceEventsBenchmark {
+  private static final String SPAN_NAME = "MySpanName";
+  private static final String ANNOTATION_DESCRIPTION = "MyAnnotation";
+  private static final String ATTRIBUTE_KEY = "MyAttributeKey";
+  private static final String ATTRIBUTE_VALUE = "MyAttributeValue";
+
+  @State(Scope.Benchmark)
+  public static class Data {
+
+    private Span linkedSpan = BlankSpan.INSTANCE;
+    private Span span = BlankSpan.INSTANCE;
+
+    @Param({"impl", "impl-lite"})
+    String implementation;
+
+    @Param({"true", "false"})
+    boolean sampled;
+
+    @Setup
+    public void setup() {
+      Tracer tracer = BenchmarksUtil.getTracer(implementation);
+      linkedSpan =
+          tracer
+              .spanBuilderWithExplicitParent(SPAN_NAME, null)
+              .setSampler(sampled ? Samplers.alwaysSample() : Samplers.neverSample())
+              .startSpan();
+      span =
+          tracer
+              .spanBuilderWithExplicitParent(SPAN_NAME, null)
+              .setSampler(sampled ? Samplers.alwaysSample() : Samplers.neverSample())
+              .startSpan();
+    }
+
+    @TearDown
+    public void doTearDown() {
+      checkState(linkedSpan != BlankSpan.INSTANCE, "Uninitialized linkedSpan");
+      checkState(span != BlankSpan.INSTANCE, "Uninitialized span");
+      linkedSpan.end();
+      span.end();
+    }
+  }
+
+  /** This benchmark attempts to measure performance of adding an attribute to the span. */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span putAttribute(Data data) {
+    data.span.putAttribute(ATTRIBUTE_KEY, AttributeValue.stringAttributeValue(ATTRIBUTE_VALUE));
+    return data.span;
+  }
+
+  /** This benchmark attempts to measure performance of adding an annotation to the span. */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span addAnnotation(Data data) {
+    data.span.addAnnotation(ANNOTATION_DESCRIPTION);
+    return data.span;
+  }
+
+  /** This benchmark attempts to measure performance of adding a network event to the span. */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span addMessageEvent(Data data) {
+    data.span.addMessageEvent(
+        io.opencensus.trace.MessageEvent.builder(Type.RECEIVED, 1)
+            .setUncompressedMessageSize(3)
+            .build());
+    return data.span;
+  }
+
+  /** This benchmark attempts to measure performance of adding a link to the span. */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span addLink(Data data) {
+    data.span.addLink(
+        Link.fromSpanContext(data.linkedSpan.getContext(), Link.Type.PARENT_LINKED_SPAN));
+    return data.span;
+  }
+}
diff --git a/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/StartEndSpanBenchmark.java b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/StartEndSpanBenchmark.java
new file mode 100644
index 0000000..02f77f5
--- /dev/null
+++ b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/StartEndSpanBenchmark.java
@@ -0,0 +1,164 @@
+/*
+ * 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.benchmarks.trace;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+
+/** Benchmarks for {@link io.opencensus.trace.SpanBuilder} and {@link Span}. */
+@State(Scope.Benchmark)
+public class StartEndSpanBenchmark {
+  private static final String SPAN_NAME = "MySpanName";
+
+  @State(Scope.Benchmark)
+  public static class Data {
+    private Tracer tracer;
+    private Span rootSpan = BlankSpan.INSTANCE;
+
+    @Param({"impl", "impl-lite"})
+    String implementation;
+
+    @Setup
+    public void setup() {
+      tracer = BenchmarksUtil.getTracer(implementation);
+
+      rootSpan =
+          tracer
+              .spanBuilderWithExplicitParent(SPAN_NAME, null)
+              .setSampler(Samplers.neverSample())
+              .startSpan();
+    }
+
+    @TearDown
+    public void doTearDown() {
+      checkState(rootSpan != BlankSpan.INSTANCE, "Uninitialized rootSpan");
+      rootSpan.end();
+    }
+  }
+
+  /**
+   * This benchmark attempts to measure performance of start/end for a non-sampled root {@code
+   * Span}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span startEndNonSampledRootSpan(Data data) {
+    Span span =
+        data.tracer
+            .spanBuilderWithExplicitParent(SPAN_NAME, null)
+            .setSampler(Samplers.neverSample())
+            .startSpan();
+    span.end();
+    return span;
+  }
+
+  /**
+   * This benchmark attempts to measure performance of start/end for a root {@code Span} with record
+   * events option.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span startEndRecordEventsRootSpan(Data data) {
+    Span span =
+        data.tracer
+            .spanBuilderWithExplicitParent(SPAN_NAME, null)
+            .setSampler(Samplers.neverSample())
+            .setRecordEvents(true)
+            .startSpan();
+    span.end();
+    return span;
+  }
+
+  /**
+   * This benchmark attempts to measure performance of start/end for a sampled root {@code Span}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span startEndSampledRootSpan(Data data) {
+    Span span = data.tracer.spanBuilder(SPAN_NAME).setSampler(Samplers.alwaysSample()).startSpan();
+    span.end();
+    return span;
+  }
+
+  /**
+   * This benchmark attempts to measure performance of start/end for a non-sampled child {@code
+   * Span}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span startEndNonSampledChildSpan(Data data) {
+    Span span =
+        data.tracer
+            .spanBuilderWithExplicitParent(SPAN_NAME, data.rootSpan)
+            .setSampler(Samplers.neverSample())
+            .startSpan();
+    span.end();
+    return span;
+  }
+
+  /**
+   * This benchmark attempts to measure performance of start/end for a child {@code Span} with
+   * record events option.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span startEndRecordEventsChildSpan(Data data) {
+    Span span =
+        data.tracer
+            .spanBuilderWithExplicitParent(SPAN_NAME, data.rootSpan)
+            .setSampler(Samplers.neverSample())
+            .setRecordEvents(true)
+            .startSpan();
+    span.end();
+    return span;
+  }
+
+  /**
+   * This benchmark attempts to measure performance of start/end for a sampled child {@code Span}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Span startEndSampledChildSpan(Data data) {
+    Span span =
+        data.tracer
+            .spanBuilderWithExplicitParent(SPAN_NAME, data.rootSpan)
+            .setSampler(Samplers.alwaysSample())
+            .startSpan();
+    span.end();
+    return span;
+  }
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..dcb006c
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,497 @@
+buildscript {
+    repositories {
+        mavenCentral()
+        mavenLocal()
+        maven {
+            url "https://plugins.gradle.org/m2/"
+        }
+    }
+    dependencies {
+        classpath 'ru.vyarus:gradle-animalsniffer-plugin:1.4.6'
+        classpath 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.16'
+        classpath "net.ltgt.gradle:gradle-apt-plugin:0.18"
+        classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
+        classpath "gradle.plugin.com.github.sherter.google-java-format:google-java-format-gradle-plugin:0.7.1"
+        classpath "me.champeau.gradle:jmh-gradle-plugin:0.4.7"
+        classpath "gradle.plugin.io.morethan.jmhreport:gradle-jmh-report:0.7.0"
+    }
+}
+
+// Display the version report using: ./gradlew dependencyUpdates
+// Also see https://github.com/ben-manes/gradle-versions-plugin.
+apply plugin: 'com.github.ben-manes.versions'
+
+// Don't use the Checker Framework by default, since it interferes with Error Prone.
+def useCheckerFramework = rootProject.hasProperty('checkerFramework')
+def useErrorProne = !useCheckerFramework
+
+subprojects {
+    apply plugin: "checkstyle"
+    apply plugin: 'maven'
+    apply plugin: 'idea'
+    apply plugin: 'eclipse'
+    apply plugin: 'java'
+    apply plugin: "signing"
+    apply plugin: "jacoco"
+    // The plugin only has an effect if a signature is specified
+    apply plugin: 'ru.vyarus.animalsniffer'
+    apply plugin: 'findbugs'
+    apply plugin: 'net.ltgt.apt'
+    apply plugin: "me.champeau.gradle.jmh"
+    apply plugin: "io.morethan.jmhreport"
+    // Plugins that require java8
+    if (JavaVersion.current().isJava8Compatible()) {
+        if (useErrorProne) {
+            apply plugin: "net.ltgt.errorprone"
+        }
+        apply plugin: 'com.github.sherter.google-java-format'
+    }
+
+    group = "io.opencensus"
+    version = "0.17.0-SNAPSHOT" // CURRENT_OPENCENSUS_VERSION
+
+    sourceCompatibility = 1.6
+    targetCompatibility = 1.6
+
+    repositories {
+        mavenCentral()
+        mavenLocal()
+    }
+
+    if (useCheckerFramework) {
+        configurations {
+            checkerFrameworkJavac {
+                description = 'a customization of the Open JDK javac compiler with additional support for type annotations'
+            }
+            checkerFrameworkAnnotatedJDK {
+                description = 'a copy of JDK classes with Checker Framework type qualifiers inserted'
+            }
+        }
+    }
+
+    [compileJava, compileTestJava, compileJmhJava].each() {
+        // We suppress the "try" warning because it disallows managing an auto-closeable with
+        // try-with-resources without referencing the auto-closeable within the try block.
+        // We suppress the "processing" warning as suggested in
+        // https://groups.google.com/forum/#!topic/bazel-discuss/_R3A9TJSoPM
+        it.options.compilerArgs += ["-Xlint:all", "-Xlint:-try", "-Xlint:-processing"]
+        if (useErrorProne) {
+            if (JavaVersion.current().isJava8Compatible()) {
+                it.options.compilerArgs += ["-XepAllDisabledChecksAsWarnings", "-XepDisableWarningsInGeneratedCode"]
+
+                // MutableMethodReturnType can suggest returning Guava types from
+                // API methods (https://github.com/google/error-prone/issues/982).
+                it.options.compilerArgs += ["-Xep:MutableMethodReturnType:OFF"]
+
+                // ReturnMissingNullable conflicts with Checker Framework null analysis.
+                it.options.compilerArgs += ["-Xep:ReturnMissingNullable:OFF"]
+
+                // OpenCensus doesn't currently use Var annotations.
+                it.options.compilerArgs += ["-Xep:Var:OFF"]
+            }
+        }
+        if (useCheckerFramework) {
+            it.options.compilerArgs += [
+                '-processor',
+		'com.google.auto.value.processor.AutoValueProcessor,org.checkerframework.checker.nullness.NullnessChecker',
+		"-Astubs=$rootDir/checker-framework/stubs"
+            ]
+        }
+        it.options.encoding = "UTF-8"
+        // Protobuf-generated code produces some warnings.
+        // https://github.com/google/protobuf/issues/2718
+        it.options.compilerArgs += ["-Xlint:-cast"]
+        if (!JavaVersion.current().isJava9()) {
+            // TODO(sebright): Enable -Werror for Java 9 once we upgrade AutoValue (issue #1017).
+            it.options.compilerArgs += ["-Werror"]
+        }
+        if (JavaVersion.current().isJava7()) {
+            // Suppress all deprecation warnings with Java 7, since there are some bugs in its handling of
+            // @SuppressWarnings. See
+            // https://stackoverflow.com/questions/26921774/how-to-avoid-deprecation-warnings-when-suppresswarningsdeprecation-doesnt
+            it.options.compilerArgs += ["-Xlint:-deprecation"]
+
+            // TODO(bdrutu): Enable for Java 7 when fix the issue with configuring bootstrap class.
+            // [options] bootstrap class path not set in conjunction with -source 1.6
+            it.options.compilerArgs += ["-Xlint:-options"]
+        }
+        if (JavaVersion.current().isJava9()) {
+            // TODO(sebright): Currently, building with Java 9 produces the following "options" warnings:
+            //
+            // :opencensus-api:compileJavawarning: [options] bootstrap class path not set in conjunction with -source 1.6
+            // warning: [options] source value 1.6 is obsolete and will be removed in a future release
+            // warning: [options] target value 1.6 is obsolete and will be removed in a future release
+            it.options.compilerArgs += ["-Xlint:-options"]
+        }
+    }
+
+    compileTestJava {
+        // serialVersionUID is basically guaranteed to be useless in tests
+        options.compilerArgs += ["-Xlint:-serial"]
+        // It undeprecates DoubleSubject.isEqualTo(Double).
+        options.compilerArgs += ["-Xlint:-deprecation"]
+    }
+
+    jar.manifest {
+        attributes('Implementation-Title': name,
+                'Implementation-Version': version,
+                'Built-By': System.getProperty('user.name'),
+                'Built-JDK': System.getProperty('java.version'),
+                'Source-Compatibility': sourceCompatibility,
+                'Target-Compatibility': targetCompatibility)
+    }
+
+    javadoc.options {
+        encoding = 'UTF-8'
+        links 'https://docs.oracle.com/javase/8/docs/api/'
+    }
+
+    ext {
+        appengineVersion = '1.9.64'
+        aspectjVersion = '1.8.11'
+        autoValueVersion = '1.4'
+        findBugsAnnotationsVersion = '3.0.1'
+        findBugsJsr305Version = '3.0.2'
+        errorProneVersion = '2.3.1'
+        grpcVersion = '1.14.0'
+        guavaVersion = '20.0'
+        googleAuthVersion = '0.11.0'
+        googleCloudBetaVersion = '0.64.0-beta'
+        googleCloudGaVersion = '1.46.0'
+        log4j2Version = '2.11.1'
+        signalfxVersion = '0.0.39'
+        springBootVersion = '1.5.15.RELEASE'
+        springCloudVersion = '1.3.4.RELEASE'
+        springVersion = '4.3.12.RELEASE'
+        prometheusVersion = '0.4.0'
+        protobufVersion = '3.5.1'
+        zipkinReporterVersion = '2.3.2'
+        jaegerReporterVersion = '0.27.0'
+        opencensusProtoVersion = '0.0.2'
+        dropwizardVersion = '3.1.2'
+
+        libraries = [
+                appengine_api: "com.google.appengine:appengine-api-1.0-sdk:${appengineVersion}",
+                aspectj: "org.aspectj:aspectjrt:${aspectjVersion}",
+                auto_value: "com.google.auto.value:auto-value:${autoValueVersion}",
+                auto_service: 'com.google.auto.service:auto-service:1.0-rc3',
+                byte_buddy: 'net.bytebuddy:byte-buddy:1.7.11',
+                config: 'com.typesafe:config:1.2.1',
+                disruptor: 'com.lmax:disruptor:3.4.1',
+                errorprone: "com.google.errorprone:error_prone_annotations:${errorProneVersion}",
+                findbugs_annotations: "com.google.code.findbugs:annotations:${findBugsAnnotationsVersion}",
+                google_auth: "com.google.auth:google-auth-library-credentials:${googleAuthVersion}",
+                google_cloud_logging: "com.google.cloud:google-cloud-logging:${googleCloudGaVersion}",
+                google_cloud_trace: "com.google.cloud:google-cloud-trace:${googleCloudBetaVersion}",
+                log4j2: "org.apache.logging.log4j:log4j-core:${log4j2Version}",
+                zipkin_reporter: "io.zipkin.reporter2:zipkin-reporter:${zipkinReporterVersion}",
+                zipkin_urlconnection: "io.zipkin.reporter2:zipkin-sender-urlconnection:${zipkinReporterVersion}",
+                jaeger_reporter: "com.uber.jaeger:jaeger-core:${jaegerReporterVersion}",
+                google_cloud_monitoring: "com.google.cloud:google-cloud-monitoring:${googleCloudGaVersion}",
+                grpc_context: "io.grpc:grpc-context:${grpcVersion}",
+                grpc_core: "io.grpc:grpc-core:${grpcVersion}",
+                grpc_netty: "io.grpc:grpc-netty:${grpcVersion}",
+                grpc_stub: "io.grpc:grpc-stub:${grpcVersion}",
+                guava: "com.google.guava:guava:${guavaVersion}",
+                jsr305: "com.google.code.findbugs:jsr305:${findBugsJsr305Version}",
+                signalfx_java: "com.signalfx.public:signalfx-java:${signalfxVersion}",
+                spring_aspects: "org.springframework:spring-aspects:${springVersion}",
+                spring_boot_starter_web: "org.springframework.boot:spring-boot-starter-web:${springBootVersion}",
+                spring_cloud_build: "org.springframework.cloud:spring-cloud-build:${springCloudVersion}",
+                spring_cloud_starter_sleuth: "org.springframework.cloud:spring-cloud-starter-sleuth:${springCloudVersion}",
+                spring_context: "org.springframework:spring-context:${springVersion}",
+                spring_context_support: "org.springframework:spring-context-support:${springVersion}",
+                prometheus_simpleclient: "io.prometheus:simpleclient:${prometheusVersion}",
+                protobuf: "com.google.protobuf:protobuf-java:${protobufVersion}",
+                opencensus_proto: "io.opencensus:opencensus-proto:${opencensusProtoVersion}",
+
+                // Test dependencies.
+                guava_testlib: "com.google.guava:guava-testlib:${guavaVersion}",
+                junit: 'junit:junit:4.12',
+                mockito: 'org.mockito:mockito-core:1.9.5',
+                spring_test: "org.springframework:spring-test:${springVersion}",
+                truth: 'com.google.truth:truth:0.30',
+                dropwizard: "io.dropwizard.metrics:metrics-core:${dropwizardVersion}",
+        ]
+    }
+
+    configurations {
+        compile {
+            // Detect Maven Enforcer's dependencyConvergence failures. We only
+            // care for artifacts used as libraries by others.
+            if (!(project.name in ['benchmarks', 'opencensus-all',
+                                   'opencensus-exporter-stats-stackdriver',
+                                   'opencensus-exporter-trace-stackdriver',
+                                   'opencensus-exporter-trace-jaeger'])) {
+                resolutionStrategy.failOnVersionConflict()
+            }
+        }
+    }
+
+    dependencies {
+        if (useCheckerFramework) {
+            ext.checkerFrameworkVersion = '2.5.5'
+
+            // 2.4.0 is the last version of the Checker Framework compiler that supports annotations
+            // in comments, though it should continue to work with newer versions of the Checker Framework.
+            // See
+            // https://github.com/census-instrumentation/opencensus-java/pull/1112#issuecomment-381366366.
+            ext.checkerFrameworkCompilerVersion = '2.4.0'
+
+            ext.jdkVersion = 'jdk8'
+            checkerFrameworkAnnotatedJDK "org.checkerframework:${jdkVersion}:${checkerFrameworkVersion}"
+            checkerFrameworkJavac "org.checkerframework:compiler:${checkerFrameworkCompilerVersion}"
+            compileOnly "org.checkerframework:checker:${checkerFrameworkVersion}"
+            compile "org.checkerframework:checker-qual:${checkerFrameworkVersion}"
+            compileOnly libraries.auto_value
+        }
+
+        compileOnly libraries.errorprone,
+                    libraries.jsr305
+
+        testCompile libraries.guava_testlib,
+                libraries.junit,
+                libraries.mockito,
+                libraries.truth
+
+    if (useErrorProne && JavaVersion.current().isJava8Compatible()) {
+            // The ErrorProne plugin defaults to the latest, which would break our
+            // build if error prone releases a new version with a new check
+            errorprone "com.google.errorprone:error_prone_core:${errorProneVersion}"
+        }
+    }
+
+    findbugs {
+        toolVersion = findBugsAnnotationsVersion
+        ignoreFailures = false   // bug free or it doesn't ship!
+        effort = 'max'
+        reportLevel = 'low'      // low = sensitive to even minor mistakes
+        omitVisitors = []        // bugs that we want to ignore
+        excludeFilter = file("$rootDir/findbugs-exclude.xml")
+    }
+    // Generate html report for findbugs.
+    findbugsMain {
+        reports {
+            xml.enabled = false
+            html.enabled = true
+        }
+    }
+    findbugsTest {
+        reports {
+            xml.enabled = false
+            html.enabled = true
+        }
+    }
+    findbugsJmh {
+        reports {
+            xml.enabled = false
+            html.enabled = true
+        }
+    }
+
+    checkstyle {
+        configFile = file("$rootDir/buildscripts/checkstyle.xml")
+        toolVersion = "8.12"
+        ignoreFailures = false
+        if (rootProject.hasProperty("checkstyle.ignoreFailures")) {
+            ignoreFailures = rootProject.properties["checkstyle.ignoreFailures"].toBoolean()
+        }
+        configProperties["rootDir"] = rootDir
+    }
+
+    // Disable checkstyle if no java8.
+    checkstyleMain.enabled = JavaVersion.current().isJava8Compatible()
+    checkstyleTest.enabled = JavaVersion.current().isJava8Compatible()
+    checkstyleJmh.enabled = JavaVersion.current().isJava8Compatible()
+
+    // Google formatter works only on java8.
+    if (JavaVersion.current().isJava8Compatible()) {
+        googleJavaFormat {
+            toolVersion '1.6'
+        }
+
+        afterEvaluate {  // Allow subproject to add more source sets.
+            tasks.googleJavaFormat {
+                source = sourceSets*.allJava
+                include '**/*.java'
+            }
+
+            tasks.verifyGoogleJavaFormat {
+                source = sourceSets*.allJava
+                include '**/*.java'
+            }
+        }
+    }
+
+    signing {
+        required false
+        sign configurations.archives
+    }
+
+    task javadocJar(type: Jar) {
+        classifier = 'javadoc'
+        from javadoc
+    }
+
+    task sourcesJar(type: Jar) {
+        classifier = 'sources'
+        from sourceSets.main.allSource
+    }
+
+    artifacts {
+        archives javadocJar, sourcesJar
+    }
+
+    jmh {
+        jmhVersion = '1.20'
+        warmupIterations = 10
+        iterations = 10
+        fork = 1
+        failOnError = true
+        resultFormat = 'JSON'
+        // Allow to run single benchmark class like:
+        // ./gradlew -PjmhIncludeSingleClass=StatsTraceContextBenchmark clean :grpc-core:jmh
+        if (project.hasProperty('jmhIncludeSingleClass')) {
+            include = [
+                    project.property('jmhIncludeSingleClass')
+            ]
+        }
+    }
+
+    jmhReport {
+        jmhResultPath = project.file("${project.buildDir}/reports/jmh/results.json")
+        jmhReportOutput = project.file("${project.buildDir}/reports/jmh")
+    }
+
+    tasks.jmh.finalizedBy tasks.jmhReport
+
+    uploadArchives {
+        repositories {
+            mavenDeployer {
+                beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+
+                def configureAuth = {
+                    if (rootProject.hasProperty('ossrhUsername') && rootProject.hasProperty('ossrhPassword')) {
+                        authentication(userName:rootProject.ossrhUsername, password: rootProject.ossrhPassword)
+                    }
+                }
+
+                repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/", configureAuth)
+
+                snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/", configureAuth)
+
+                pom.project {
+                    name "OpenCensus"
+                    packaging 'jar'
+                    description project.description
+                    url 'https://github.com/census-instrumentation/opencensus-java'
+
+                    scm {
+                        connection 'scm:svn:https://github.com/census-instrumentation/opencensus-java'
+                        developerConnection 'scm:git:git@github.com/census-instrumentation/opencensus-java'
+                        url 'https://github.com/census-instrumentation/opencensus-java'
+                    }
+
+                    licenses {
+                        license {
+                            name 'The Apache License, Version 2.0'
+                            url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+                        }
+                    }
+
+                    developers {
+                        developer {
+                            id 'io.opencensus'
+                            name 'OpenCensus Contributors'
+                            email 'census-developers@googlegroups.com'
+                            url 'opencensus.io'
+                            // https://issues.gradle.org/browse/GRADLE-2719
+                            organization = 'OpenCensus Authors'
+                            organizationUrl 'https://www.opencensus.io'
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // Upload the following artifacts only:
+    uploadArchives.onlyIf {
+        name in ['opencensus-api',
+                 'opencensus-contrib-agent',
+                 'opencensus-contrib-appengine-standard-util',
+                 'opencensus-contrib-dropwizard',
+                 'opencensus-contrib-exemplar-util',
+                 'opencensus-contrib-grpc-metrics',
+                 'opencensus-contrib-grpc-util',
+                 'opencensus-contrib-http-util',
+                 'opencensus-contrib-log-correlation-log4j2',
+                 'opencensus-contrib-log-correlation-stackdriver',
+                 'opencensus-contrib-monitored-resource-util',
+                 'opencensus-contrib-spring',
+                 'opencensus-contrib-spring-sleuth-v1x',
+                 'opencensus-contrib-zpages',
+                 'opencensus-exporter-stats-prometheus',
+                 'opencensus-exporter-stats-signalfx',
+                 'opencensus-exporter-stats-stackdriver',
+                 'opencensus-exporter-trace-instana',
+                 'opencensus-exporter-trace-logging',
+                 'opencensus-exporter-trace-ocagent',
+                 'opencensus-exporter-trace-stackdriver',
+                 'opencensus-exporter-trace-zipkin',
+                 'opencensus-exporter-trace-jaeger',
+                 'opencensus-impl-core',
+                 'opencensus-impl-lite',
+                 'opencensus-impl',
+                 'opencensus-testing']
+    }
+
+    // At a test failure, log the stack trace to the console so that we don't
+    // have to open the HTML in a browser.
+    test {
+        testLogging {
+            exceptionFormat = 'full'
+            showExceptions true
+            showCauses true
+            showStackTraces true
+        }
+        maxHeapSize = '1500m'
+    }
+
+    if (useCheckerFramework) {
+        allprojects {
+            tasks.withType(JavaCompile).all { JavaCompile compile ->
+                compile.doFirst {
+                    compile.options.compilerArgs += [
+                        '-Xmaxerrs', '10000',
+                        "-Xbootclasspath/p:${configurations.checkerFrameworkAnnotatedJDK.asPath}",
+                        "-AskipDefs=\\.AutoValue_|^io.opencensus.contrib.appengine.standard.util.TraceIdProto\$|^io.opencensus.contrib.appengine.standard.util.TraceProto\$",
+                        "-AinvariantArrays"
+                    ]
+                    options.fork = true
+                    options.forkOptions.jvmArgs += ["-Xbootclasspath/p:${configurations.checkerFrameworkJavac.asPath}"]
+                }
+            }
+        }
+    }
+
+    // For projects that depend on gRPC during test execution, make sure to
+    // also configure ALPN if running on a platform (e.g. FreeBSD) that is not
+    // supported by io.netty:netty-tcnative-boringssl-static:jar. Also see:
+    // https://github.com/grpc/grpc-java/blob/master/SECURITY.md#tls-with-jdk-jetty-alpnnpn
+    if (project.name in ['opencensus-exporter-stats-stackdriver',
+                         'opencensus-exporter-trace-stackdriver']) {
+        def os = org.gradle.internal.os.OperatingSystem.current()
+        if (!os.isLinux() && !os.isWindows() && !os.isMacOsX()) {
+            configurations {
+                alpn
+            }
+            dependencies {
+                alpn 'org.mortbay.jetty.alpn:jetty-alpn-agent:2.0.7'
+            }
+            test {
+                jvmArgs "-javaagent:${configurations.alpn.asPath}"
+            }
+        }
+    }
+}
diff --git a/buildscripts/checkstyle.license b/buildscripts/checkstyle.license
new file mode 100644
index 0000000..27bac1a
--- /dev/null
+++ b/buildscripts/checkstyle.license
@@ -0,0 +1,15 @@
+^/\*$
+^ \* Copyright \d\d\d\d(-\d\d)?, 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\.$
+^ \*/$
\ No newline at end of file
diff --git a/buildscripts/checkstyle.xml b/buildscripts/checkstyle.xml
new file mode 100644
index 0000000..50b146e
--- /dev/null
+++ b/buildscripts/checkstyle.xml
@@ -0,0 +1,277 @@
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+          "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
+          "https://checkstyle.org/dtds/configuration_1_3.dtd">
+
+<!--
+    Checkstyle configuration that checks the Google coding conventions from Google Java Style
+    that can be found at https://google.github.io/styleguide/javaguide.html.
+
+    Checkstyle is very configurable. Be sure to read the documentation at
+    http://checkstyle.sf.net (or in your downloaded distribution).
+
+    To completely disable a check, just comment it out or delete it from the file.
+
+    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
+ -->
+
+<module name = "Checker">
+    <property name="charset" value="UTF-8"/>
+
+    <property name="severity" value="error"/>
+
+
+    <module name="RegexpHeader">
+        <property name="headerFile" value="${rootDir}/buildscripts/checkstyle.license"/>
+        <property name="fileExtensions" value="java"/>
+    </module>
+
+    <property name="fileExtensions" value="java, properties, xml"/>
+    <!-- Checks for whitespace                               -->
+    <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+    <module name="FileTabCharacter">
+        <property name="eachLine" value="true"/>
+    </module>
+
+    <module name="TreeWalker">
+        <module name="OuterTypeFilename"/>
+        <module name="IllegalTokenText">
+            <property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
+            <property name="format"
+             value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
+            <property name="message"
+             value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
+        </module>
+        <module name="AvoidEscapedUnicodeCharacters">
+            <property name="allowEscapesForControlCharacters" value="true"/>
+            <property name="allowByTailComment" value="true"/>
+            <property name="allowNonPrintableEscapes" value="true"/>
+        </module>
+        <module name="LineLength">
+            <property name="max" value="100"/>
+            <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
+        </module>
+        <module name="AvoidStarImport"/>
+        <module name="RedundantImport"/>
+        <module name="OneTopLevelClass"/>
+        <module name="NoLineWrap"/>
+        <module name="EmptyBlock">
+            <property name="option" value="TEXT"/>
+            <property name="tokens"
+             value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
+        </module>
+        <module name="NeedBraces"/>
+        <module name="LeftCurly"/>
+        <module name="RightCurly">
+            <property name="id" value="RightCurlySame"/>
+            <property name="tokens"
+             value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
+                    LITERAL_DO"/>
+        </module>
+        <module name="RightCurly">
+            <property name="id" value="RightCurlyAlone"/>
+            <property name="option" value="alone"/>
+            <property name="tokens"
+             value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
+                    INSTANCE_INIT"/>
+        </module>
+        <module name="WhitespaceAround">
+            <property name="allowEmptyConstructors" value="true"/>
+            <property name="allowEmptyMethods" value="true"/>
+            <property name="allowEmptyTypes" value="true"/>
+            <property name="allowEmptyLoops" value="true"/>
+            <message key="ws.notFollowed"
+             value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
+            <message key="ws.notPreceded"
+             value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
+        </module>
+        <module name="OneStatementPerLine"/>
+        <module name="MultipleVariableDeclarations"/>
+        <module name="ArrayTypeStyle"/>
+        <!-- <!-\- This rule conflicts with Error Prone's exhaustiveness checking. -\-> -->
+        <!-- <module name="MissingSwitchDefault"/> -->
+        <module name="FallThrough"/>
+        <module name="UpperEll"/>
+        <module name="ModifierOrder"/>
+        <module name="EmptyLineSeparator">
+            <property name="allowNoEmptyLineBetweenFields" value="true"/>
+        </module>
+        <module name="SeparatorWrap">
+            <property name="id" value="SeparatorWrapDot"/>
+            <property name="tokens" value="DOT"/>
+            <property name="option" value="nl"/>
+        </module>
+        <module name="SeparatorWrap">
+            <property name="id" value="SeparatorWrapComma"/>
+            <property name="tokens" value="COMMA"/>
+            <property name="option" value="EOL"/>
+        </module>
+        <module name="SeparatorWrap">
+            <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
+            <property name="id" value="SeparatorWrapEllipsis"/>
+            <property name="tokens" value="ELLIPSIS"/>
+            <property name="option" value="EOL"/>
+        </module>
+        <module name="SeparatorWrap">
+            <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
+            <property name="id" value="SeparatorWrapArrayDeclarator"/>
+            <property name="tokens" value="ARRAY_DECLARATOR"/>
+            <property name="option" value="EOL"/>
+        </module>
+        <module name="SeparatorWrap">
+            <property name="id" value="SeparatorWrapMethodRef"/>
+            <property name="tokens" value="METHOD_REF"/>
+            <property name="option" value="nl"/>
+        </module>
+        <module name="PackageName">
+            <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
+            <message key="name.invalidPattern"
+             value="Package name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="TypeName">
+            <message key="name.invalidPattern"
+             value="Type name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="MemberName">
+            <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
+            <message key="name.invalidPattern"
+             value="Member name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="ParameterName">
+            <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
+            <message key="name.invalidPattern"
+             value="Parameter name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="LambdaParameterName">
+            <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
+            <message key="name.invalidPattern"
+                     value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="CatchParameterName">
+            <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
+            <message key="name.invalidPattern"
+             value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="LocalVariableName">
+            <property name="tokens" value="VARIABLE_DEF"/>
+            <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
+            <message key="name.invalidPattern"
+             value="Local variable name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="ClassTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
+            <message key="name.invalidPattern"
+             value="Class type name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="MethodTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
+            <message key="name.invalidPattern"
+             value="Method type name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="InterfaceTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
+            <message key="name.invalidPattern"
+             value="Interface type name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="NoFinalizer"/>
+        <module name="GenericWhitespace">
+            <message key="ws.followed"
+             value="GenericWhitespace ''{0}'' is followed by whitespace."/>
+            <message key="ws.preceded"
+             value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
+            <message key="ws.illegalFollow"
+             value="GenericWhitespace ''{0}'' should followed by whitespace."/>
+            <message key="ws.notPreceded"
+             value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
+        </module>
+        <!-- <!-\- Checkstyle indentation rules conflict with google-java-format: -\-> -->
+        <!-- <module name="Indentation"> -->
+        <!--     <property name="basicOffset" value="2"/> -->
+        <!--     <property name="braceAdjustment" value="0"/> -->
+        <!--     <property name="caseIndent" value="2"/> -->
+        <!--     <property name="throwsIndent" value="4"/> -->
+        <!--     <property name="lineWrappingIndentation" value="4"/> -->
+        <!--     <property name="arrayInitIndent" value="2"/> -->
+        <!-- </module> -->
+        <module name="AbbreviationAsWordInName">
+            <property name="ignoreFinal" value="false"/>
+            <property name="allowedAbbreviationLength" value="1"/>
+        </module>
+        <module name="OverloadMethodsDeclarationOrder"/>
+        <!-- <!-\- Many unit tests define all variables at the start of the method. -\-> -->
+        <!-- <module name="VariableDeclarationUsageDistance"/> -->
+        <module name="CustomImportOrder">
+            <property name="sortImportsInGroupAlphabetically" value="true"/>
+            <property name="separateLineBetweenGroups" value="true"/>
+            <property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
+        </module>
+        <module name="MethodParamPad"/>
+        <module name="NoWhitespaceBefore">
+            <property name="tokens"
+             value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
+            <property name="allowLineBreaks" value="true"/>
+        </module>
+        <module name="ParenPad"/>
+        <module name="OperatorWrap">
+            <property name="option" value="NL"/>
+            <property name="tokens"
+             value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
+                    LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
+        </module>
+        <module name="AnnotationLocation">
+            <property name="id" value="AnnotationLocationMostCases"/>
+            <property name="tokens"
+             value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
+        </module>
+        <module name="AnnotationLocation">
+            <property name="id" value="AnnotationLocationVariables"/>
+            <property name="tokens" value="VARIABLE_DEF"/>
+            <property name="allowSamelineMultipleAnnotations" value="true"/>
+        </module>
+        <module name="NonEmptyAtclauseDescription"/>
+        <module name="JavadocTagContinuationIndentation"/>
+        <module name="SummaryJavadoc">
+            <property name="forbiddenSummaryFragments"
+             value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
+        </module>
+        <module name="JavadocParagraph"/>
+        <module name="AtclauseOrder">
+            <property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
+            <property name="target"
+             value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
+        </module>
+        <module name="JavadocMethod">
+            <property name="scope" value="public"/>
+            <property name="allowMissingParamTags" value="true"/>
+            <property name="allowMissingThrowsTags" value="true"/>
+            <property name="allowMissingReturnTag" value="true"/>
+            <property name="minLineCount" value="2"/>
+            <!-- <!-\- Too restrictive for tests -\-> -->
+            <!-- <property name="allowedAnnotations" value="Override, Test"/ -->
+            <property name="allowedAnnotations"
+                      value="Override, Test, Before, After, BeforeClass, AfterClass, Setup,
+                             TearDown"/>
+            <property name="allowThrowsTagsForSubclasses" value="true"/>
+        </module>
+        <module name="MethodName">
+            <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
+            <message key="name.invalidPattern"
+             value="Method name ''{0}'' must match pattern ''{1}''."/>
+        </module>
+        <module name="SingleLineJavadoc">
+            <!-- <!-\- Wrong interpretation of the style guide; -\-> -->
+            <!-- <property name="ignoreInlineTags" value="false"/ -->
+        </module>
+        <module name="EmptyCatchBlock">
+            <property name="exceptionVariableName" value="expected"/>
+        </module>
+        <module name="CommentsIndentation"/>
+        <module name="SuppressWarningsHolder"/>
+        <module name="ImportControl">
+            <property name="file" value="${rootDir}/buildscripts/import-control.xml"/>
+            <property name="path" value="^.*[\\/]src[\\/]main[\\/]java[\\/].*$"/>
+        </module>
+        <module name="SuppressionCommentFilter"/>
+    </module>
+    <module name="SuppressWarningsFilter"/>
+</module>
diff --git a/buildscripts/codecov.yml b/buildscripts/codecov.yml
new file mode 100644
index 0000000..a2c8d61
--- /dev/null
+++ b/buildscripts/codecov.yml
@@ -0,0 +1,2 @@
+ignore:
+  - "impl_core/src/main/java/io/opencensus/implcore/internal/VarInt.java"  # ignore VarInt
diff --git a/buildscripts/import-control.xml b/buildscripts/import-control.xml
new file mode 100644
index 0000000..d545878
--- /dev/null
+++ b/buildscripts/import-control.xml
@@ -0,0 +1,252 @@
+<?xml version="1.0"?>
+<!DOCTYPE import-control PUBLIC
+    "-//Puppy Crawl//DTD Import Control 1.3//EN"
+    "http://checkstyle.sourceforge.net/dtds/import_control_1_3.dtd">
+
+<!--
+
+General guidelines on imports:
+
+- 'stats' depends on 'tags', but 'tags' shouldn't depend on 'stats' or 'trace'.
+  'stats'/'tags' and 'trace' should remain independent, where possible.
+
+- Packages should not be split between artifacts.
+
+- 'internal' packages should only be imported by packages within the same
+  artifact.
+
+- Since we are trying to remove dependencies on Guava (issue #1113), we should
+  avoid adding any new Guava imports here, especially in the API.
+
+-->
+
+<import-control pkg="io.opencensus">
+  <allow pkg="com.google.auto.value"/>
+  <allow pkg="com.google.errorprone.annotations"/>
+  <allow pkg="java"/>
+  <allow pkg="javax"/>
+  <allow class="io.grpc.Context"/>
+  <subpackage name="common">
+    <allow pkg="io.opencensus.common"/>
+  </subpackage>
+  <subpackage name="internal">
+    <allow pkg="io.opencensus.common"/>
+    <allow pkg="io.opencensus.internal"/>
+  </subpackage>
+  <subpackage name="tags">
+    <allow pkg="io.opencensus.common"/>
+    <allow pkg="io.opencensus.internal"/>
+    <allow pkg="io.opencensus.tags"/>
+  </subpackage>
+  <subpackage name="metrics">
+    <allow pkg="io.opencensus.internal"/>
+    <allow pkg="io.opencensus.common"/>
+    <allow pkg="io.opencensus.metrics"/>
+  </subpackage>
+  <subpackage name="stats">
+    <allow pkg="io.opencensus.common"/>
+    <allow pkg="io.opencensus.internal"/>
+    <allow pkg="io.opencensus.stats"/>
+    <allow pkg="io.opencensus.tags"/>
+  </subpackage>
+  <subpackage name="trace">
+    <allow pkg="io.opencensus.common"/>
+    <allow pkg="io.opencensus.internal"/>
+    <allow pkg="io.opencensus.trace"/>
+
+    <!-- These dependencies on impl/implcore are only needed by -->
+    <!-- io.opencensus.trace.TraceComponentImpl and io.opencensus.trace.TraceComponentImplLite, -->
+    <!-- which are deprecated. -->
+    <allow class="io.opencensus.impl.internal.DisruptorEventQueue"/>
+    <allow class="io.opencensus.impl.trace.internal.ThreadLocalRandomHandler"/>
+    <allow class="io.opencensus.implcore.common.MillisClock"/>
+    <allow class="io.opencensus.implcore.internal.SimpleEventQueue"/>
+    <allow class="io.opencensus.implcore.trace.TraceComponentImplBase"/>
+    <allow class="io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler"/>
+  </subpackage>
+  <subpackage name="contrib">
+    <allow pkg="com.google.common"/>
+    <allow pkg="io.opencensus.common"/>
+    <subpackage name="agent">
+      <allow pkg="com.google.auto"/>
+      <allow pkg="com.typesafe.config"/>
+      <allow pkg="edu.umd.cs.findbugs.annotations"/>
+      <allow pkg="io.opencensus.contrib.agent"/>
+      <allow pkg="io.opencensus.trace"/>
+      <allow pkg="net.bytebuddy"/>
+    </subpackage>
+    <subpackage name="appengine.standard.util">
+      <allow pkg="com.google.apphosting"/>
+      <allow pkg="io.opencensus.trace"/>
+    </subpackage>
+    <subpackage name="exemplar.util">
+      <allow pkg="io.opencensus.stats"/>
+      <allow pkg="io.opencensus.trace"/>
+    </subpackage>
+    <subpackage name="grpc.metrics">
+      <allow pkg="io.opencensus.contrib.grpc.metrics"/>
+      <allow pkg="io.opencensus.stats"/>
+      <allow pkg="io.opencensus.tags"/>
+    </subpackage>
+    <subpackage name="http.util">
+      <allow pkg="io.opencensus.contrib.http.util"/>
+      <allow pkg="io.opencensus.stats"/>
+      <allow pkg="io.opencensus.tags"/>
+      <allow pkg="io.opencensus.trace"/>
+    </subpackage>
+    <subpackage name="logcorrelation.log4j2">
+      <allow pkg="io.opencensus.contrib.logcorrelation.log4j2"/>
+      <allow pkg="io.opencensus.trace"/>
+      <disallow pkg="org.apache.logging.log4j.core.impl"/>
+      <allow pkg="org.apache.logging.log4j"/>
+    </subpackage>
+    <subpackage name="logcorrelation.stackdriver">
+      <allow pkg="com.google.cloud"/>
+      <allow pkg="io.opencensus.trace"/>
+    </subpackage>
+    <subpackage name="spring">
+      <allow pkg="io.opencensus.trace"/>
+      <allow pkg="org.aspectj.lang"/>
+      <allow pkg="org.aspectj.lang.annotation"/>
+      <allow pkg="org.aspectj.lang.reflect"/>
+      <allow pkg="org.springframework.beans.factory.annotation"/>
+      <subpackage name="sleuth">
+        <allow pkg="io.opencensus.trace"/>
+        <allow pkg="org.apache.commons.logging"/>
+        <allow pkg="org.springframework.beans.factory.annotation"/>
+        <allow pkg="org.springframework.beans.factory.config"/>
+        <allow pkg="org.springframework.boot.autoconfigure"/>
+        <allow pkg="org.springframework.boot.context"/>
+        <allow pkg="org.springframework.context.annotation"/>
+        <allow pkg="org.springframework.boot.context.properties"/>
+        <allow pkg="org.springframework.cloud.sleuth"/>
+        <allow pkg="org.springframework.core"/>
+      </subpackage>
+    </subpackage>
+    <subpackage name="zpages">
+      <allow pkg="com.sun.net.httpserver"/>
+      <allow pkg="io.opencensus.contrib.grpc.metrics"/>
+      <allow pkg="io.opencensus.contrib.zpages"/>
+      <allow pkg="io.opencensus.stats"/>
+      <allow pkg="io.opencensus.tags"/>
+      <allow pkg="io.opencensus.trace"/>
+    </subpackage>
+    <subpackage name="monitoredresource.util">
+      <allow pkg="io.opencensus.contrib.monitoredresource.util"/>
+    </subpackage>
+    <subpackage name="dropwizard">
+      <allow pkg="io.opencensus.contrib.dropwizard"/>
+      <allow pkg="io.opencensus.metrics"/>
+      <allow pkg="io.opencensus.implcore"/>
+      <allow pkg="io.opencensus.internal"/>
+      <allow pkg="com.codahale.metrics"/>
+    </subpackage>
+  </subpackage>
+  <subpackage name="exporter">
+    <allow pkg="com.google.common"/>
+    <allow pkg="io.opencensus.common"/>
+    <subpackage name="stats">
+      <allow pkg="io.opencensus.stats"/>
+      <allow pkg="io.opencensus.tags"/>
+      <subpackage name="prometheus">
+        <allow pkg="io.opencensus.exporter.stats.prometheus"/>
+        <allow pkg="io.opencensus.trace"/>
+        <allow pkg="io.prometheus.client"/>
+      </subpackage>
+      <subpackage name="signalfx">
+        <allow pkg="com.signalfx"/>
+        <allow pkg="io.opencensus.exporter.stats.signalfx"/>
+        <allow pkg="io.opencensus.trace"/>
+      </subpackage>
+      <subpackage name="stackdriver">
+        <allow pkg="com.google"/>
+        <allow pkg="io.opencensus.exporter.stats.stackdriver"/>
+        <allow pkg="io.opencensus.trace"/>
+        <allow pkg="io.opencensus.contrib.monitoredresource.util"/>
+      </subpackage>
+    </subpackage>
+    <subpackage name="trace">
+      <allow pkg="io.opencensus.trace"/>
+      <subpackage name="instana">
+        <allow pkg="io.opencensus.exporter.trace.instana"/>
+      </subpackage>
+      <subpackage name="jaeger">
+        <allow pkg="com.uber.jaeger"/>
+        <allow pkg="io.opencensus.exporter.trace.jaeger"/>
+        <allow pkg="org.apache.thrift"/>
+      </subpackage>
+      <subpackage name="ocagent">
+        <allow pkg="com.google.protobuf"/>
+        <allow pkg="io.grpc"/>
+        <allow pkg="io.opencensus.contrib.monitoredresource.util"/>
+        <allow pkg="io.opencensus.contrib.opencensus.proto.util"/>
+        <allow pkg="io.opencensus.exporter.trace.ocagent"/>
+        <allow pkg="io.opencensus.proto"/>
+        <allow pkg="io.opencensus.trace"/>
+      </subpackage>
+      <subpackage name="stackdriver">
+        <allow pkg="com.google"/>
+        <allow pkg="io.opencensus.exporter.trace.stackdriver"/>
+        <allow pkg="io.opencensus.contrib.monitoredresource.util"/>
+      </subpackage>
+      <subpackage name="zipkin">
+        <allow pkg="io.opencensus.exporter.trace.zipkin"/>
+        <allow pkg="zipkin2"/>
+      </subpackage>
+    </subpackage>
+  </subpackage>
+  <subpackage name="implcore">
+    <allow pkg="com.google.common"/>
+    <allow pkg="io.opencensus.common"/>
+    <allow pkg="io.opencensus.implcore"/>
+    <allow pkg="io.opencensus.metrics"/>
+    <allow pkg="io.opencensus.stats"/>
+    <allow pkg="io.opencensus.tags"/>
+    <allow pkg="io.opencensus.trace"/>
+  </subpackage>
+  <subpackage name="impl">
+    <allow pkg="com.lmax.disruptor"/>
+    <allow pkg="io.opencensus.common"/>
+    <allow pkg="io.opencensus.impl"/>
+    <allow pkg="io.opencensus.implcore"/>
+    <allow pkg="io.opencensus.metrics"/>
+    <allow pkg="io.opencensus.stats"/>
+    <allow pkg="io.opencensus.tags"/>
+    <allow pkg="io.opencensus.trace"/>
+  </subpackage>
+  <subpackage name="impllite">
+    <allow pkg="io.opencensus.common"/>
+    <allow pkg="io.opencensus.implcore"/>
+    <allow pkg="io.opencensus.impllite"/>
+    <allow pkg="io.opencensus.metrics"/>
+    <allow pkg="io.opencensus.stats"/>
+    <allow pkg="io.opencensus.tags"/>
+    <allow pkg="io.opencensus.trace"/>
+  </subpackage>
+  <subpackage name="testing">
+    <allow pkg="com.google.common"/>
+    <allow pkg="io.opencensus.common"/>
+    <subpackage name="common">
+      <allow pkg="io.opencensus.testing.common"/>
+    </subpackage>
+    <subpackage name="export">
+      <allow pkg="io.opencensus.stats"/>
+      <allow pkg="io.opencensus.tags"/>
+      <allow pkg="io.opencensus.testing.export"/>
+      <allow pkg="io.opencensus.trace"/>
+    </subpackage>
+  </subpackage>
+  <subpackage name="examples">
+    <allow pkg="com.google.common"/>
+    <allow pkg="io.grpc"/>
+    <allow pkg="io.opencensus.common"/>
+    <allow pkg="io.opencensus.contrib"/>
+    <allow pkg="io.opencensus.examples"/>
+    <allow pkg="io.opencensus.exporter"/>
+    <allow pkg="io.opencensus.stats"/>
+    <allow pkg="io.opencensus.tags"/>
+    <allow pkg="io.opencensus.testing.export"/>
+    <allow pkg="io.opencensus.trace"/>
+    <allow pkg="io.prometheus"/>
+  </subpackage>
+</import-control>
diff --git a/buildscripts/kokoro/linux.cfg b/buildscripts/kokoro/linux.cfg
new file mode 100644
index 0000000..0d9e253
--- /dev/null
+++ b/buildscripts/kokoro/linux.cfg
@@ -0,0 +1,5 @@
+# Config file for internal CI
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux.sh"
+timeout_mins: 60
\ No newline at end of file
diff --git a/buildscripts/kokoro/linux.sh b/buildscripts/kokoro/linux.sh
new file mode 100755
index 0000000..e8aa21b
--- /dev/null
+++ b/buildscripts/kokoro/linux.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# This file is used for Linux builds.
+# To run locally:
+#  ./buildscripts/kokoro/linux.sh
+
+# This script assumes `set -e`. Removing it may lead to undefined behavior.
+set -exu -o pipefail
+
+# It would be nicer to use 'readlink -f' here but osx does not support it.
+readonly OPENCENSUS_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)"
+
+# cd to the root dir of opencensus-java
+cd $(dirname $0)/../..
+
+# Run tests
+./gradlew clean build
+
+OS=`uname`
+# Check the example only on Linux.
+if [ "$OS" = "Linux" ] ; then
+    pushd examples; ./gradlew clean assemble check --stacktrace; popd
+fi
diff --git a/buildscripts/kokoro/linux_build.cfg b/buildscripts/kokoro/linux_build.cfg
new file mode 100644
index 0000000..ddd1593
--- /dev/null
+++ b/buildscripts/kokoro/linux_build.cfg
@@ -0,0 +1,19 @@
+# Config file for child task BUILD
+
+env_vars {
+  key: "TASK"
+  value: "BUILD"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
+
+before_action {
+  fetch_keystore {
+    keystore_resource {
+      keystore_config_id: 73495
+      keyname: "codecov-auth-token"
+    }
+  }
+}
diff --git a/buildscripts/kokoro/linux_example_bazel.cfg b/buildscripts/kokoro/linux_example_bazel.cfg
new file mode 100644
index 0000000..3f4c872
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_bazel.cfg
@@ -0,0 +1,10 @@
+# Config file for child task BUILD_EXAMPLES_BAZEL
+
+env_vars {
+  key: "TASK"
+  value: "BUILD_EXAMPLES_BAZEL"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_example_format.cfg b/buildscripts/kokoro/linux_example_format.cfg
new file mode 100644
index 0000000..6f9a3dc
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_format.cfg
@@ -0,0 +1,9 @@
+# Config file for child task CHECK_EXAMPLES_FORMAT
+env_vars {
+  key: "TASK"
+  value: "CHECK_EXAMPLES_FORMAT"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_example_gradle.cfg b/buildscripts/kokoro/linux_example_gradle.cfg
new file mode 100644
index 0000000..7c14df7
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_gradle.cfg
@@ -0,0 +1,10 @@
+# Config file for child task BUILD_EXAMPLES_GRADLE
+
+env_vars {
+  key: "TASK"
+  value: "BUILD_EXAMPLES_GRADLE"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_example_license.cfg b/buildscripts/kokoro/linux_example_license.cfg
new file mode 100644
index 0000000..19cc67d
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_license.cfg
@@ -0,0 +1,10 @@
+# Config file for child task CHECK_EXAMPLES_LICENSE
+
+env_vars {
+  key: "TASK"
+  value: "CHECK_EXAMPLES_LICENSE"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_example_maven.cfg b/buildscripts/kokoro/linux_example_maven.cfg
new file mode 100644
index 0000000..98f4a3b
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_maven.cfg
@@ -0,0 +1,10 @@
+# Config file for child task BUILD_EXAMPLES_MAVEN
+
+env_vars {
+  key: "TASK"
+  value: "BUILD_EXAMPLES_MAVEN"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_framework.cfg b/buildscripts/kokoro/linux_framework.cfg
new file mode 100644
index 0000000..112fc20
--- /dev/null
+++ b/buildscripts/kokoro/linux_framework.cfg
@@ -0,0 +1,10 @@
+# Config file for child task CHECKER_FRAMEWORK
+
+env_vars {
+  key: "TASK"
+  value: "CHECKER_FRAMEWORK"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_git_history.cfg b/buildscripts/kokoro/linux_git_history.cfg
new file mode 100644
index 0000000..5677835
--- /dev/null
+++ b/buildscripts/kokoro/linux_git_history.cfg
@@ -0,0 +1,10 @@
+# Config file for child task CHECK_GIT_HISTORY
+
+env_vars {
+  key: "TASK"
+  value: "CHECK_GIT_HISTORY"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_presubmit.sh b/buildscripts/kokoro/linux_presubmit.sh
new file mode 100755
index 0000000..bb1281b
--- /dev/null
+++ b/buildscripts/kokoro/linux_presubmit.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+# This file is used for Linux builds.
+# It expects TASK environment variable is defined.
+# To run locally:
+#  ./buildscripts/kokoro/linux.sh
+
+# This script assumes `set -e`. Removing it may lead to undefined behavior.
+set -exu -o pipefail
+
+# It would be nicer to use 'readlink -f' here but osx does not support it.
+readonly OPENCENSUS_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)"
+
+# cd to the root dir of opencensus-java
+cd $(dirname $0)/../..
+
+valid_tasks() {
+  echo "Valid tasks are"
+  echo ""
+  echo "- BUILD"
+  echo "- BUILD_EXAMPLES_BAZEL"
+  echo "- BUILD_EXAMPLES_GRADLE"
+  echo "- BUILD_EXAMPLES_MAVEN"
+  echo "- CHECKER_FRAMEWORK"
+  echo "- CHECK_EXAMPLES_FORMAT"
+  echo "- CHECK_EXAMPLES_LICENSE"
+  echo "- CHECK_GIT_HISTORY"
+}
+
+if [[ ! -v TASK ]]; then
+  set +x
+  echo "TASK not set in environment"
+  valid_tasks
+  exit 1
+fi
+
+case "$TASK" in
+  "CHECK_GIT_HISTORY")
+    python ./scripts/check-git-history.py
+    ;;
+  "BUILD")
+    ./gradlew clean assemble --stacktrace
+    ./gradlew check :opencensus-all:jacocoTestReport
+    ./gradlew verGJF
+
+    # Run codecoverage reporting only if the script is running
+    # as a part of KOKORO BUILD. If it is outside of kokoro
+    # then there is no access to the codecov token and hence
+    # there is no point in running it.
+    if [[ -v KOKORO_BUILD_NUMBER ]]; then
+      # Get token from file located at
+      # $KOKORO_KEYSTORE_DIR/73495_codecov-auth-token
+      if [ -f $KOKORO_KEYSTORE_DIR/73495_codecov-auth-token ] ; then
+        curl -s https://codecov.io/bash | bash -s -- -Z -t @$KOKORO_KEYSTORE_DIR/73495_codecov-auth-token
+      else
+        echo "Codecov token file not found"
+        exit 1
+      fi
+    else
+      echo "Skipping codecov reporting"
+    fi
+    ;;
+  "CHECKER_FRAMEWORK")
+    ./gradlew clean assemble -PcheckerFramework=true
+    ;;
+  "CHECK_EXAMPLES_LICENSE")
+    curl -L -o checkstyle-8.12-all.jar https://github.com/checkstyle/checkstyle/releases/download/checkstyle-8.12/checkstyle-8.12-all.jar
+    java -DrootDir=. -jar checkstyle-8.12-all.jar -c buildscripts/checkstyle.xml examples/src/
+    ;;
+  "CHECK_EXAMPLES_FORMAT")
+    curl -L -o google-java-format-1.5-all-deps.jar \
+      https://github.com/google/google-java-format/releases/download/google-java-format-1.5/google-java-format-1.5-all-deps.jar
+    java -jar google-java-format-1.5-all-deps.jar --set-exit-if-changed --dry-run `find examples/src/ -name '*.java'`
+    ;;
+  "BUILD_EXAMPLES_GRADLE")
+    pushd examples && ./gradlew clean assemble --stacktrace && popd
+    ;;
+  "BUILD_EXAMPLES_MAVEN")
+    pushd examples && mvn clean package appassembler:assemble -e && popd
+    ;;
+  "BUILD_EXAMPLES_BAZEL")
+    pushd examples && bazel clean && bazel build :all && popd
+    ;;
+  *)
+    set +x
+    echo "Unknown task $TASK"
+    valid_tasks
+    exit 1
+    ;;
+esac
diff --git a/buildscripts/kokoro/macos.cfg b/buildscripts/kokoro/macos.cfg
new file mode 100644
index 0000000..fe3a980
--- /dev/null
+++ b/buildscripts/kokoro/macos.cfg
@@ -0,0 +1,6 @@
+# Config file for internal CI
+
+# Same script is used for macos as it is for Linux.
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/windows.bat b/buildscripts/kokoro/windows.bat
new file mode 100755
index 0000000..7787df0
--- /dev/null
+++ b/buildscripts/kokoro/windows.bat
@@ -0,0 +1,21 @@
+@rem ##########################################################################
+@rem
+@rem Script to set up Kokoro worker and run Windows tests
+@rem
+@rem ##########################################################################
+@rem
+@rem To run locally execute 'buildscript\kokoro\windows.bat'.
+type c:\VERSION
+
+@rem Enter repo root
+cd /d %~dp0\..\..
+
+@rem Clear JAVA_HOME to prevent a different Java version from being used
+set JAVA_HOME=
+set PATH=C:\Program Files\java\jdk1.8.0_152\bin;%PATH%
+
+cmd.exe /C "%cd%\gradlew.bat" clean build || exit /b 1
+pushd examples
+cmd.exe /C "%cd%\gradlew.bat" clean assemble check --stacktrace || exit /b 1
+popd
+
diff --git a/buildscripts/kokoro/windows.cfg b/buildscripts/kokoro/windows.cfg
new file mode 100644
index 0000000..e5ff9b0
--- /dev/null
+++ b/buildscripts/kokoro/windows.cfg
@@ -0,0 +1,5 @@
+# Config file for internal CI
+
+# Location of the continuous windows batch script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/windows.bat"
+timeout_mins: 60
diff --git a/checker-framework/stubs/grpc.astub b/checker-framework/stubs/grpc.astub
new file mode 100644
index 0000000..f8581eb
--- /dev/null
+++ b/checker-framework/stubs/grpc.astub
@@ -0,0 +1,12 @@
+package io.grpc;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+class Context {
+  static <T> Key<@Nullable T> key(String name);
+  static <T> Key<T> keyWithDefault(String name, T defaultValue);
+  class Key<T> {
+    T get(Context context);
+    T get();
+  }
+}
diff --git a/checker-framework/stubs/guava.astub b/checker-framework/stubs/guava.astub
new file mode 100644
index 0000000..42ed251
--- /dev/null
+++ b/checker-framework/stubs/guava.astub
@@ -0,0 +1,14 @@
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+package com.google.common.base;
+
+class Strings {
+  @EnsuresNonNullIf(result = false, expression = "#1")
+  static boolean isNullOrEmpty(@Nullable String str);
+}
+
+class Preconditions {
+  static <T extends @NonNull Object> T checkNotNull(T reference, @Nullable Object errorMessage);
+}
diff --git a/checker-framework/stubs/log4j.astub b/checker-framework/stubs/log4j.astub
new file mode 100644
index 0000000..20b3240
--- /dev/null
+++ b/checker-framework/stubs/log4j.astub
@@ -0,0 +1,8 @@
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+package org.apache.logging.log4j;
+
+class ThreadContext {
+  @Nullable
+  static ReadOnlyThreadContextMap getThreadContextMap();
+}
diff --git a/checker-framework/stubs/org-springframework-cloud-sleuth.astub b/checker-framework/stubs/org-springframework-cloud-sleuth.astub
new file mode 100644
index 0000000..61d2fa1
--- /dev/null
+++ b/checker-framework/stubs/org-springframework-cloud-sleuth.astub
@@ -0,0 +1,19 @@
+package org.springframework.cloud.sleuth;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.springframework.cloud.sleuth.Sampler;
+import org.springframework.cloud.sleuth.Span;
+
+interface Tracer {
+  @Nullable Span close(@Nullable Span span);
+  @Nullable Span continueSpan(@Nullable Span span);
+  @Nullable Span createSpan(String name);
+  @Nullable Span createSpan(String name, @Nullable Sampler sampler);
+  @Nullable Span createSpan(String name, @Nullable Span parent);
+  @Nullable Span detach(@Nullable Span span);
+  @Nullable Span getCurrentSpan();
+}
+
+class Span {
+  Span (Span span, @Nullable Span parent);
+}
diff --git a/checker-framework/stubs/org-springframework-cloud-sleuth.log.astub b/checker-framework/stubs/org-springframework-cloud-sleuth.log.astub
new file mode 100644
index 0000000..9497f6f
--- /dev/null
+++ b/checker-framework/stubs/org-springframework-cloud-sleuth.log.astub
@@ -0,0 +1,9 @@
+package org.springframework.cloud.sleuth.log;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.springframework.cloud.sleuth.Span;
+
+interface SpanLogger {
+  void logStartedSpan(@Nullable Span parent, Span span);
+  void logStoppedSpan(@Nullable Span parent, Span span);
+}
diff --git a/contrib/agent/README.md b/contrib/agent/README.md
new file mode 100644
index 0000000..f24c28a
--- /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 0000000..11271a4
--- /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 0000000..7cab559
--- /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 0000000..f718f49
--- /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 0000000..163f3cd
--- /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 0000000..7e8787c
--- /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 0000000..7c2d442
--- /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 0000000..706c6d3
--- /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 0000000..54a8244
--- /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 0000000..49c568e
--- /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 0000000..7367b85
--- /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 0000000..46fe395
--- /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 0000000..57d4efc
--- /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 0000000..2e737be
--- /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 0000000..363dbbd
--- /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 0000000..aeae259
--- /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 0000000..f1363a2
--- /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 0000000..71e8127
--- /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 0000000..8a6d8a6
--- /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 0000000..17a5b1d
--- /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 0000000..1e1429c
--- /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 0000000..5eb197e
--- /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 0000000..b4beba8
--- /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 0000000..139c10f
--- /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 0000000..4a68845
--- /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 0000000..336f70b
--- /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 0000000..e178124
--- /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 0000000..26eb696
--- /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 0000000..4ed7120
--- /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 0000000..f1ca350
--- /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 0000000..75d8940
--- /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 0000000..4585c37
--- /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 0000000..3fa1249
--- /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 0000000..07319bb
--- /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
diff --git a/contrib/appengine_standard_util/README.md b/contrib/appengine_standard_util/README.md
new file mode 100644
index 0000000..3ff5a0a
--- /dev/null
+++ b/contrib/appengine_standard_util/README.md
@@ -0,0 +1,35 @@
+# OpenCensus AppEngine Standard Util
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus AppEngine Standard Util for Java* is a collection of utilities for trace
+instrumentation when working with [AppEngine][appengine-url].
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-appengine-standard-util</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-appengine-standard-util:0.16.1'
+```
+
+[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-appengine-standard-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-appengine-standard-util
+[appengine-url]: https://appengine.google.com/
diff --git a/contrib/appengine_standard_util/build.gradle b/contrib/appengine_standard_util/build.gradle
new file mode 100644
index 0000000..a5c122a
--- /dev/null
+++ b/contrib/appengine_standard_util/build.gradle
@@ -0,0 +1,52 @@
+description = 'OpenCensus AppEngine Standard Util'
+
+apply plugin: 'java'
+apply plugin: 'com.google.protobuf'
+
+def protocVersion = '3.5.1-1'
+
+buildscript {
+    repositories {
+        maven { url "https://plugins.gradle.org/m2/" }
+    }
+    dependencies {
+        classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.5"
+    }
+}
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.7
+    it.targetCompatibility = 1.7
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.appengine_api,
+            libraries.guava,
+            libraries.protobuf
+
+    signature "org.codehaus.mojo.signature:java18:+@signature"
+}
+
+protobuf {
+    protoc {
+        // The artifact spec for the Protobuf Compiler
+        artifact = "com.google.protobuf:protoc:${protocVersion}"
+    }
+
+    generatedFilesBaseDir = "$projectDir/gen_gradle/src"
+
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                java {
+                    option 'annotate_code'
+                }
+            }
+        }
+    }
+}
+
+clean {
+    delete protobuf.generatedFilesBaseDir
+}
diff --git a/contrib/appengine_standard_util/src/main/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtils.java b/contrib/appengine_standard_util/src/main/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtils.java
new file mode 100644
index 0000000..9fac951
--- /dev/null
+++ b/contrib/appengine_standard_util/src/main/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtils.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2018, 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.appengine.standard.util;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.apphosting.api.CloudTraceContext;
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility class to convert between {@link io.opencensus.trace.SpanContext} and {@link
+ * CloudTraceContext}.
+ *
+ * @since 0.14
+ */
+public final class AppEngineCloudTraceContextUtils {
+  private static final byte[] INVALID_TRACE_ID =
+      TraceIdProto.newBuilder().setHi(0).setLo(0).build().toByteArray();
+  private static final long INVALID_SPAN_ID = 0L;
+  private static final long INVALID_TRACE_MASK = 0L;
+  private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+
+  @VisibleForTesting
+  static final CloudTraceContext INVALID_CLOUD_TRACE_CONTEXT =
+      new CloudTraceContext(INVALID_TRACE_ID, INVALID_SPAN_ID, INVALID_TRACE_MASK);
+
+  /**
+   * Converts AppEngine {@code CloudTraceContext} to {@code SpanContext}.
+   *
+   * @param cloudTraceContext the AppEngine {@code CloudTraceContext}.
+   * @return the converted {@code SpanContext}.
+   * @since 0.14
+   */
+  public static SpanContext fromCloudTraceContext(CloudTraceContext cloudTraceContext) {
+    checkNotNull(cloudTraceContext, "cloudTraceContext");
+
+    try {
+      // Extract the trace ID from the binary protobuf CloudTraceContext#traceId.
+      TraceIdProto traceIdProto = TraceIdProto.parseFrom(cloudTraceContext.getTraceId());
+      ByteBuffer traceIdBuf = ByteBuffer.allocate(TraceId.SIZE);
+      traceIdBuf.putLong(traceIdProto.getHi());
+      traceIdBuf.putLong(traceIdProto.getLo());
+      ByteBuffer spanIdBuf = ByteBuffer.allocate(SpanId.SIZE);
+      spanIdBuf.putLong(cloudTraceContext.getSpanId());
+
+      return SpanContext.create(
+          TraceId.fromBytes(traceIdBuf.array()),
+          SpanId.fromBytes(spanIdBuf.array()),
+          TraceOptions.builder().setIsSampled(cloudTraceContext.isTraceEnabled()).build(),
+          TRACESTATE_DEFAULT);
+    } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Converts {@code SpanContext} to AppEngine {@code CloudTraceContext}.
+   *
+   * @param spanContext the {@code SpanContext}.
+   * @return the converted AppEngine {@code CloudTraceContext}.
+   * @since 0.14
+   */
+  public static CloudTraceContext toCloudTraceContext(SpanContext spanContext) {
+    checkNotNull(spanContext, "spanContext");
+
+    ByteBuffer traceIdBuf = ByteBuffer.wrap(spanContext.getTraceId().getBytes());
+    TraceIdProto traceIdProto =
+        TraceIdProto.newBuilder().setHi(traceIdBuf.getLong()).setLo(traceIdBuf.getLong()).build();
+    ByteBuffer spanIdBuf = ByteBuffer.wrap(spanContext.getSpanId().getBytes());
+
+    return new CloudTraceContext(
+        traceIdProto.toByteArray(),
+        spanIdBuf.getLong(),
+        spanContext.getTraceOptions().isSampled() ? 1L : 0L);
+  }
+
+  private AppEngineCloudTraceContextUtils() {}
+}
diff --git a/contrib/appengine_standard_util/src/main/proto/trace_id.proto b/contrib/appengine_standard_util/src/main/proto/trace_id.proto
new file mode 100644
index 0000000..35e2e08
--- /dev/null
+++ b/contrib/appengine_standard_util/src/main/proto/trace_id.proto
@@ -0,0 +1,10 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "io.opencensus.contrib.appengine.standard.util";
+option java_outer_classname = "TraceProto";
+
+message TraceIdProto {
+  fixed64 hi = 1;
+  fixed64 lo = 2;
+}
diff --git a/contrib/appengine_standard_util/src/test/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtilsTest.java b/contrib/appengine_standard_util/src/test/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtilsTest.java
new file mode 100644
index 0000000..dc53d8f
--- /dev/null
+++ b/contrib/appengine_standard_util/src/test/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtilsTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2018, 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.appengine.standard.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.appengine.standard.util.AppEngineCloudTraceContextUtils.INVALID_CLOUD_TRACE_CONTEXT;
+
+import com.google.apphosting.api.CloudTraceContext;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AppEngineCloudTraceContextUtils}. */
+@RunWith(JUnit4.class)
+public class AppEngineCloudTraceContextUtilsTest {
+  @Test
+  public void toFromSampledCloudTraceContext() {
+    CloudTraceContext cloudTraceContext =
+        new CloudTraceContext(
+            // Protobuf-encoded upper and lower 64 bits of the example trace ID
+            // fae1c6346b9cf9a272cb6504b5a10dcc/123456789.
+            new byte[] {
+              (byte) 0x09,
+              (byte) 0xa2,
+              (byte) 0xf9,
+              (byte) 0x9c,
+              (byte) 0x6b,
+              (byte) 0x34,
+              (byte) 0xc6,
+              (byte) 0xe1,
+              (byte) 0xfa,
+              (byte) 0x11,
+              (byte) 0xcc,
+              (byte) 0x0d,
+              (byte) 0xa1,
+              (byte) 0xb5,
+              (byte) 0x04,
+              (byte) 0x65,
+              (byte) 0xcb,
+              (byte) 0x72
+            },
+            Long.MIN_VALUE,
+            // Trace enabled.
+            1L);
+
+    SpanContext spanContext =
+        AppEngineCloudTraceContextUtils.fromCloudTraceContext(cloudTraceContext);
+
+    assertThat(spanContext)
+        .isEqualTo(
+            SpanContext.create(
+                TraceId.fromLowerBase16("fae1c6346b9cf9a272cb6504b5a10dcc"),
+                SpanId.fromLowerBase16("8000000000000000"),
+                TraceOptions.builder().setIsSampled(true).build()));
+
+    // CloudTraceContext does not implement equals, so need to check every argument.
+    CloudTraceContext newCloudTraceContext =
+        AppEngineCloudTraceContextUtils.toCloudTraceContext(spanContext);
+    assertThat(newCloudTraceContext.getTraceId()).isEqualTo(cloudTraceContext.getTraceId());
+    assertThat(newCloudTraceContext.getSpanId()).isEqualTo(cloudTraceContext.getSpanId());
+    assertThat(newCloudTraceContext.getTraceMask()).isEqualTo(cloudTraceContext.getTraceMask());
+  }
+
+  @Test
+  public void toFromNotSampledCloudTraceContext() {
+    CloudTraceContext cloudTraceContext =
+        new CloudTraceContext(
+            // Protobuf-encoded upper and lower 64 bits of the example trace ID
+            // fae1c6346b9cf9a272cb6504b5a10dcc/123456789.
+            new byte[] {
+              (byte) 0x09,
+              (byte) 0xa2,
+              (byte) 0xf9,
+              (byte) 0x9c,
+              (byte) 0x6b,
+              (byte) 0x34,
+              (byte) 0xc6,
+              (byte) 0xe1,
+              (byte) 0xfa,
+              (byte) 0x11,
+              (byte) 0xcc,
+              (byte) 0x0d,
+              (byte) 0xa1,
+              (byte) 0xb5,
+              (byte) 0x04,
+              (byte) 0x65,
+              (byte) 0xcb,
+              (byte) 0x72
+            },
+            Long.MIN_VALUE,
+            // Trace disabled.
+            0L);
+
+    SpanContext spanContext =
+        AppEngineCloudTraceContextUtils.fromCloudTraceContext(cloudTraceContext);
+
+    assertThat(spanContext)
+        .isEqualTo(
+            SpanContext.create(
+                TraceId.fromLowerBase16("fae1c6346b9cf9a272cb6504b5a10dcc"),
+                SpanId.fromLowerBase16("8000000000000000"),
+                TraceOptions.builder().setIsSampled(false).build()));
+
+    // CloudTraceContext does not implement equals, so need to check every argument.
+    assertThat(
+            cloudTraceContextEquals(
+                AppEngineCloudTraceContextUtils.toCloudTraceContext(spanContext),
+                cloudTraceContext))
+        .isTrue();
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void toCloudTraceContext_Null() {
+    AppEngineCloudTraceContextUtils.fromCloudTraceContext(null);
+  }
+
+  @Test
+  public void toCloudTraceContext_Invalid() {
+    assertThat(AppEngineCloudTraceContextUtils.fromCloudTraceContext(INVALID_CLOUD_TRACE_CONTEXT))
+        .isEqualTo(SpanContext.INVALID);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void fromCloudTraceContext_Null() {
+    AppEngineCloudTraceContextUtils.toCloudTraceContext(null);
+  }
+
+  @Test
+  public void fromCloudTraceContext_Invalid() {
+    assertThat(
+            cloudTraceContextEquals(
+                AppEngineCloudTraceContextUtils.toCloudTraceContext(SpanContext.INVALID),
+                INVALID_CLOUD_TRACE_CONTEXT))
+        .isTrue();
+  }
+
+  private static boolean cloudTraceContextEquals(CloudTraceContext obj1, CloudTraceContext obj2) {
+    return Arrays.equals(obj1.getTraceId(), obj2.getTraceId())
+        && obj1.getSpanId() == obj2.getSpanId()
+        && obj1.getTraceMask() == obj2.getTraceMask();
+  }
+}
diff --git a/contrib/dropwizard/README.md b/contrib/dropwizard/README.md
new file mode 100644
index 0000000..0010d00
--- /dev/null
+++ b/contrib/dropwizard/README.md
@@ -0,0 +1,112 @@
+# OpenCensus DropWizard Util for Java
+
+The *OpenCensus DropWizard Util for Java* provides an easy way to translate Dropwizard metrics to
+OpenCensus.
+
+## Quickstart
+
+### Prerequisites
+
+Assuming, you already have both the OpenCensus and Dropwizard client libraries setup and working
+inside your application.
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-dropwizard</artifactId>
+    <version>0.17.0</version>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-dropwizard:0.17.0'
+```
+
+### And the following code:
+
+```java
+import java.util.Collections;
+
+public class YourClass {
+  // Create registry for Dropwizard metrics.
+  static final com.codahale.metrics.MetricRegistry codahaleRegistry =
+    new com.codahale.metrics.MetricRegistry();
+
+  // Create a Dropwizard counter.
+  static final com.codahale.metrics.Counter requests = codahaleRegistry.counter("requests");
+
+  public static void main(String[] args) {
+
+    // Increment the requests.
+    requests.inc();
+
+    // Hook the Dropwizard registry into the OpenCensus registry
+    // via the DropWizardMetrics metric producer.
+    io.opencensus.metrics.Metrics.getExportComponent().getMetricProducerManager().add(
+          new io.opencensus.contrib.dropwizard.DropWizardMetrics(
+            Collections.singletonList(codahaleRegistry)));
+
+  }
+}
+```
+
+## Translation to OpenCensus Metrics
+
+This section describes how each of the DropWizard metrics translate into OpenCensus metrics.
+
+### DropWizard Counters
+
+Given a DropWizard Counter with name `cache_evictions`, the following values are reported:
+
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_cache_evictions_counter)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+(ex: Collected from Dropwizard (metric=cache_evictions, type=com.codahale.metrics.Counter))
+* type: GAUGE_INT64
+* unit: 1
+
+Note: OpenCensus's CUMULATIVE_INT64 type represent monotonically increasing values. Since
+DropWizard Counter goes up/down, it make sense to report them as OpenCensus GAUGE_INT64.
+
+### DropWizard Gauges
+
+Given a DropWizard Gauge with name `line_requests`, the following values are reported:
+
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_line_requests_gauge)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+* type: GAUGE_INT64 or GAUGE_DOUBLE
+* unit: 1
+
+Note: For simplicity, OpenCensus uses GAUGE_DOUBLE type for any Number and GAUGE_INT64
+type for Boolean values.
+
+### DropWizard Meters
+
+Given a DropWizard Meter with name `get_requests`, the following values are reported:
+
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_get_requests_meter)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+* type: CUMULATIVE_INT64
+* unit: 1
+
+### DropWizard Histograms
+
+Given a DropWizard Histogram with name `results`, the following values are reported:
+
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_results_histogram)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+* type: SUMMARY
+* unit: 1
+
+### DropWizard Timers
+
+Given a DropWizard Timer with name `requests`, the following values are reported:
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_requests_timer)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+* type: SUMMARY
+* unit: 1
diff --git a/contrib/dropwizard/build.gradle b/contrib/dropwizard/build.gradle
new file mode 100644
index 0000000..7da41cb
--- /dev/null
+++ b/contrib/dropwizard/build.gradle
@@ -0,0 +1,17 @@
+description = 'OpenCensus dropwizard util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.7
+    it.targetCompatibility = 1.7
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            project(':opencensus-impl-core')
+
+    compile libraries.dropwizard
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
diff --git a/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardMetrics.java b/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardMetrics.java
new file mode 100644
index 0000000..d923183
--- /dev/null
+++ b/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardMetrics.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2018, 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.dropwizard;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Timer;
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Utils;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.MetricProducer;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.Summary;
+import io.opencensus.metrics.export.Summary.Snapshot;
+import io.opencensus.metrics.export.Summary.Snapshot.ValueAtPercentile;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+
+/**
+ * Collects DropWizard metrics from a list {@link com.codahale.metrics.MetricRegistry}s.
+ *
+ * <p>A {@link io.opencensus.metrics.export.MetricProducer} that wraps a DropWizardMetrics.
+ *
+ * @since 0.17
+ */
+public class DropWizardMetrics extends MetricProducer {
+  @DefaultVisibilityForTesting static final String DEFAULT_UNIT = "1";
+  private final List<com.codahale.metrics.MetricRegistry> metricRegistryList;
+  private final Clock clock;
+  private final Timestamp cumulativeStartTimestamp;
+
+  /**
+   * Hook the Dropwizard registry into the OpenCensus registry.
+   *
+   * @param metricRegistryList a list of {@link com.codahale.metrics.MetricRegistry}s.
+   * @since 0.17
+   */
+  public DropWizardMetrics(List<com.codahale.metrics.MetricRegistry> metricRegistryList) {
+    Utils.checkNotNull(metricRegistryList, "metricRegistryList");
+    Utils.checkListElementNotNull(metricRegistryList, "metricRegistryList");
+    this.metricRegistryList = metricRegistryList;
+    clock = MillisClock.getInstance();
+    cumulativeStartTimestamp = clock.now();
+  }
+
+  /**
+   * Returns a {@code Metric} collected from {@link Gauge}.
+   *
+   * @param dropwizardName the metric name.
+   * @param gauge the gauge object to collect.
+   * @return a {@code Metric}.
+   */
+  @SuppressWarnings("rawtypes")
+  private @Nullable Metric collectGauge(String dropwizardName, Gauge gauge) {
+    String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "gauge");
+    String metricDescription = DropWizardUtils.generateFullMetricDescription(dropwizardName, gauge);
+
+    // Figure out which gauge instance and call the right method to get value
+    Type type;
+    Value value;
+
+    Object obj = gauge.getValue();
+    if (obj instanceof Number) {
+      type = Type.GAUGE_DOUBLE;
+      value = Value.doubleValue(((Number) obj).doubleValue());
+    } else if (obj instanceof Boolean) {
+      type = Type.GAUGE_INT64;
+      value = Value.longValue(((Boolean) obj) ? 1 : 0);
+    } else {
+      // Ignoring Gauge (gauge.getKey()) with unhandled type.
+      return null;
+    }
+
+    MetricDescriptor metricDescriptor =
+        MetricDescriptor.create(
+            metricName, metricDescription, DEFAULT_UNIT, type, Collections.<LabelKey>emptyList());
+    TimeSeries timeSeries =
+        TimeSeries.createWithOnePoint(
+            Collections.<LabelValue>emptyList(), Point.create(value, clock.now()), null);
+    return Metric.createWithOneTimeSeries(metricDescriptor, timeSeries);
+  }
+
+  /**
+   * Returns a {@code Metric} collected from {@link Counter}.
+   *
+   * @param dropwizardName the metric name.
+   * @param counter the counter object to collect.
+   * @return a {@code Metric}.
+   */
+  private Metric collectCounter(String dropwizardName, Counter counter) {
+    String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "counter");
+    String metricDescription =
+        DropWizardUtils.generateFullMetricDescription(dropwizardName, counter);
+
+    MetricDescriptor metricDescriptor =
+        MetricDescriptor.create(
+            metricName,
+            metricDescription,
+            DEFAULT_UNIT,
+            Type.GAUGE_INT64,
+            Collections.<LabelKey>emptyList());
+    TimeSeries timeSeries =
+        TimeSeries.createWithOnePoint(
+            Collections.<LabelValue>emptyList(),
+            Point.create(Value.longValue(counter.getCount()), clock.now()),
+            null);
+    return Metric.createWithOneTimeSeries(metricDescriptor, timeSeries);
+  }
+
+  /**
+   * Returns a {@code Metric} collected from {@link Meter}.
+   *
+   * @param dropwizardName the metric name.
+   * @param meter the meter object to collect
+   * @return a {@code Metric}.
+   */
+  private Metric collectMeter(String dropwizardName, Meter meter) {
+    String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "meter");
+    String metricDescription = DropWizardUtils.generateFullMetricDescription(dropwizardName, meter);
+
+    MetricDescriptor metricDescriptor =
+        MetricDescriptor.create(
+            metricName,
+            metricDescription,
+            DEFAULT_UNIT,
+            Type.CUMULATIVE_INT64,
+            Collections.<LabelKey>emptyList());
+    TimeSeries timeSeries =
+        TimeSeries.createWithOnePoint(
+            Collections.<LabelValue>emptyList(),
+            Point.create(Value.longValue(meter.getCount()), clock.now()),
+            null);
+
+    return Metric.createWithOneTimeSeries(metricDescriptor, timeSeries);
+  }
+
+  /**
+   * Returns a {@code Metric} collected from {@link Histogram}.
+   *
+   * @param dropwizardName the metric name.
+   * @param histogram the histogram object to collect
+   * @return a {@code Metric}.
+   */
+  private Metric collectHistogram(String dropwizardName, Histogram histogram) {
+    String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "histogram");
+    String metricDescription =
+        DropWizardUtils.generateFullMetricDescription(dropwizardName, histogram);
+    return collectSnapshotAndCount(
+        metricName, metricDescription, histogram.getSnapshot(), histogram.getCount());
+  }
+
+  /**
+   * Returns a {@code Metric} collected from {@link Timer}.
+   *
+   * @param dropwizardName the metric name.
+   * @param timer the timer object to collect
+   * @return a {@code Metric}.
+   */
+  private Metric collectTimer(String dropwizardName, Timer timer) {
+    String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "timer");
+    String metricDescription = DropWizardUtils.generateFullMetricDescription(dropwizardName, timer);
+    return collectSnapshotAndCount(
+        metricName, metricDescription, timer.getSnapshot(), timer.getCount());
+  }
+
+  /**
+   * Returns a {@code Metric} collected from {@link Snapshot}.
+   *
+   * @param metricName the metric name.
+   * @param metricDescription the metric description.
+   * @param codahaleSnapshot the snapshot object to collect
+   * @param count the value or count
+   * @return a {@code Metric}.
+   */
+  private Metric collectSnapshotAndCount(
+      String metricName,
+      String metricDescription,
+      com.codahale.metrics.Snapshot codahaleSnapshot,
+      long count) {
+    List<ValueAtPercentile> valueAtPercentiles =
+        Arrays.asList(
+            ValueAtPercentile.create(50.0, codahaleSnapshot.getMedian()),
+            ValueAtPercentile.create(75.0, codahaleSnapshot.get75thPercentile()),
+            ValueAtPercentile.create(98.0, codahaleSnapshot.get98thPercentile()),
+            ValueAtPercentile.create(99.0, codahaleSnapshot.get99thPercentile()),
+            ValueAtPercentile.create(99.9, codahaleSnapshot.get999thPercentile()));
+
+    Snapshot snapshot = Snapshot.create((long) codahaleSnapshot.size(), 0.0, valueAtPercentiles);
+    Point point =
+        Point.create(Value.summaryValue(Summary.create(count, 0.0, snapshot)), clock.now());
+
+    // TODO(mayurkale): OPTIMIZATION: Cache the MetricDescriptor objects.
+    MetricDescriptor metricDescriptor =
+        MetricDescriptor.create(
+            metricName,
+            metricDescription,
+            DEFAULT_UNIT,
+            Type.SUMMARY,
+            Collections.<LabelKey>emptyList());
+    TimeSeries timeSeries =
+        TimeSeries.createWithOnePoint(
+            Collections.<LabelValue>emptyList(), point, cumulativeStartTimestamp);
+
+    return Metric.createWithOneTimeSeries(metricDescriptor, timeSeries);
+  }
+
+  @Override
+  @SuppressWarnings("rawtypes")
+  public Collection<Metric> getMetrics() {
+    ArrayList<Metric> metrics = new ArrayList<Metric>();
+
+    for (com.codahale.metrics.MetricRegistry metricRegistry : metricRegistryList) {
+      for (Entry<String, Counter> counterEntry : metricRegistry.getCounters().entrySet()) {
+        metrics.add(collectCounter(counterEntry.getKey(), counterEntry.getValue()));
+      }
+
+      for (Entry<String, Gauge> gaugeEntry : metricRegistry.getGauges().entrySet()) {
+        Metric metric = collectGauge(gaugeEntry.getKey(), gaugeEntry.getValue());
+        if (metric != null) {
+          metrics.add(metric);
+        }
+      }
+
+      for (Entry<String, Meter> counterEntry : metricRegistry.getMeters().entrySet()) {
+        metrics.add(collectMeter(counterEntry.getKey(), counterEntry.getValue()));
+      }
+
+      for (Entry<String, Histogram> counterEntry : metricRegistry.getHistograms().entrySet()) {
+        metrics.add(collectHistogram(counterEntry.getKey(), counterEntry.getValue()));
+      }
+
+      for (Entry<String, Timer> counterEntry : metricRegistry.getTimers().entrySet()) {
+        metrics.add(collectTimer(counterEntry.getKey(), counterEntry.getValue()));
+      }
+    }
+
+    return metrics;
+  }
+}
diff --git a/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardUtils.java b/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardUtils.java
new file mode 100644
index 0000000..372e5c6
--- /dev/null
+++ b/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018, 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.dropwizard;
+
+import com.codahale.metrics.Metric;
+
+/** Util methods for generating the metric name(unique) and description. */
+final class DropWizardUtils {
+  private static final String SOURCE = "codahale";
+  private static final char DELIMITER = '_';
+
+  /**
+   * Returns the metric name.
+   *
+   * @param name the initial metric name
+   * @param type the initial type of the metric.
+   * @return a string the unique metric name
+   */
+  static String generateFullMetricName(String name, String type) {
+    return SOURCE + DELIMITER + name + DELIMITER + type;
+  }
+
+  /**
+   * Returns the metric description.
+   *
+   * @param metricName the initial metric name
+   * @param metric the codahale metric class.
+   * @return a String the custom metric description
+   */
+  static String generateFullMetricDescription(String metricName, Metric metric) {
+    return "Collected from "
+        + SOURCE
+        + " (metric="
+        + metricName
+        + ", type="
+        + metric.getClass().getName()
+        + ")";
+  }
+
+  private DropWizardUtils() {}
+}
diff --git a/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardMetricsTest.java b/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardMetricsTest.java
new file mode 100644
index 0000000..2b41e9b
--- /dev/null
+++ b/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardMetricsTest.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2018, 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.dropwizard;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.dropwizard.DropWizardMetrics.DEFAULT_UNIT;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Timer;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Summary;
+import io.opencensus.metrics.export.Summary.Snapshot;
+import io.opencensus.metrics.export.Summary.Snapshot.ValueAtPercentile;
+import io.opencensus.metrics.export.Value;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DropWizardMetrics}. */
+@RunWith(JUnit4.class)
+public class DropWizardMetricsTest {
+
+  private com.codahale.metrics.MetricRegistry metricRegistry;
+  DropWizardMetrics dropWizardMetrics;
+
+  @Before
+  public void setUp() throws Exception {
+    metricRegistry = new com.codahale.metrics.MetricRegistry();
+    dropWizardMetrics = new DropWizardMetrics(Collections.singletonList((metricRegistry)));
+  }
+
+  @Test
+  public void collect() throws InterruptedException {
+
+    // create dropwizard metrics
+    Counter evictions = metricRegistry.counter("cache_evictions");
+    evictions.inc();
+    evictions.inc(3);
+    evictions.dec();
+    evictions.dec(2);
+
+    Gauge<Integer> integerGauge =
+        new Gauge<Integer>() {
+          @Override
+          public Integer getValue() {
+            return 1234;
+          }
+        };
+    metricRegistry.register("integer_gauge", integerGauge);
+
+    Gauge<Double> doubleGauge =
+        new Gauge<Double>() {
+          @Override
+          public Double getValue() {
+            return 1.234D;
+          }
+        };
+    metricRegistry.register("double_gauge", doubleGauge);
+
+    Gauge<Long> longGauge =
+        new Gauge<Long>() {
+          @Override
+          public Long getValue() {
+            return 1234L;
+          }
+        };
+    metricRegistry.register("long_gauge", longGauge);
+
+    Gauge<Float> floatGauge =
+        new Gauge<Float>() {
+          @Override
+          public Float getValue() {
+            return 0.1234F;
+          }
+        };
+    metricRegistry.register("float_gauge", floatGauge);
+
+    Gauge<Boolean> boolGauge =
+        new Gauge<Boolean>() {
+          @Override
+          public Boolean getValue() {
+            return Boolean.TRUE;
+          }
+        };
+    metricRegistry.register("boolean_gauge", boolGauge);
+
+    Meter getRequests = metricRegistry.meter("get_requests");
+    getRequests.mark();
+    getRequests.mark();
+
+    Histogram resultCounts = metricRegistry.histogram("result");
+    resultCounts.update(200);
+
+    Timer timer = metricRegistry.timer("requests");
+    Timer.Context context = timer.time();
+    Thread.sleep(1L);
+    context.stop();
+
+    ArrayList<Metric> metrics = new ArrayList<Metric>(dropWizardMetrics.getMetrics());
+    assertThat(metrics.size()).isEqualTo(9);
+
+    assertThat(metrics.get(0).getMetricDescriptor())
+        .isEqualTo(
+            MetricDescriptor.create(
+                "codahale_cache_evictions_counter",
+                "Collected from codahale (metric=cache_evictions, "
+                    + "type=com.codahale.metrics.Counter)",
+                DEFAULT_UNIT,
+                Type.GAUGE_INT64,
+                Collections.<LabelKey>emptyList()));
+    assertThat(metrics.get(0).getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metrics.get(0).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+    assertThat(metrics.get(0).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metrics.get(0).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(Value.longValue(1));
+    assertThat(metrics.get(0).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+    assertThat(metrics.get(1).getMetricDescriptor())
+        .isEqualTo(
+            MetricDescriptor.create(
+                "codahale_boolean_gauge_gauge",
+                "Collected from codahale (metric=boolean_gauge, "
+                    + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$5)",
+                DEFAULT_UNIT,
+                Type.GAUGE_INT64,
+                Collections.<LabelKey>emptyList()));
+    assertThat(metrics.get(1).getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metrics.get(1).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+    assertThat(metrics.get(1).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metrics.get(1).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(Value.longValue(1));
+    assertThat(metrics.get(1).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+    assertThat(metrics.get(2).getMetricDescriptor())
+        .isEqualTo(
+            MetricDescriptor.create(
+                "codahale_double_gauge_gauge",
+                "Collected from codahale (metric=double_gauge, "
+                    + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$2)",
+                DEFAULT_UNIT,
+                Type.GAUGE_DOUBLE,
+                Collections.<LabelKey>emptyList()));
+    assertThat(metrics.get(2).getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metrics.get(2).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+    assertThat(metrics.get(2).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metrics.get(2).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(Value.doubleValue(1.234));
+    assertThat(metrics.get(2).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+    assertThat(metrics.get(3).getMetricDescriptor())
+        .isEqualTo(
+            MetricDescriptor.create(
+                "codahale_float_gauge_gauge",
+                "Collected from codahale (metric=float_gauge, "
+                    + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$4)",
+                DEFAULT_UNIT,
+                Type.GAUGE_DOUBLE,
+                Collections.<LabelKey>emptyList()));
+    assertThat(metrics.get(3).getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metrics.get(3).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+    assertThat(metrics.get(3).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metrics.get(3).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(Value.doubleValue(0.1234000027179718));
+    assertThat(metrics.get(3).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+    assertThat(metrics.get(4).getMetricDescriptor())
+        .isEqualTo(
+            MetricDescriptor.create(
+                "codahale_integer_gauge_gauge",
+                "Collected from codahale (metric=integer_gauge, "
+                    + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$1)",
+                DEFAULT_UNIT,
+                Type.GAUGE_DOUBLE,
+                Collections.<LabelKey>emptyList()));
+    assertThat(metrics.get(4).getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metrics.get(4).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+    assertThat(metrics.get(4).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metrics.get(4).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(Value.doubleValue(1234.0));
+    assertThat(metrics.get(4).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+    assertThat(metrics.get(5).getMetricDescriptor())
+        .isEqualTo(
+            MetricDescriptor.create(
+                "codahale_long_gauge_gauge",
+                "Collected from codahale (metric=long_gauge, "
+                    + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$3)",
+                DEFAULT_UNIT,
+                Type.GAUGE_DOUBLE,
+                Collections.<LabelKey>emptyList()));
+    assertThat(metrics.get(5).getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metrics.get(5).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+    assertThat(metrics.get(5).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metrics.get(5).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(Value.doubleValue(1234.0));
+    assertThat(metrics.get(5).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+    assertThat(metrics.get(6).getMetricDescriptor())
+        .isEqualTo(
+            MetricDescriptor.create(
+                "codahale_get_requests_meter",
+                "Collected from codahale (metric=get_requests, "
+                    + "type=com.codahale.metrics.Meter)",
+                DEFAULT_UNIT,
+                Type.CUMULATIVE_INT64,
+                Collections.<LabelKey>emptyList()));
+    assertThat(metrics.get(6).getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metrics.get(6).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+    assertThat(metrics.get(6).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metrics.get(6).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(Value.longValue(2));
+    assertThat(metrics.get(6).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+    assertThat(metrics.get(7).getMetricDescriptor())
+        .isEqualTo(
+            MetricDescriptor.create(
+                "codahale_result_histogram",
+                "Collected from codahale (metric=result, " + "type=com.codahale.metrics.Histogram)",
+                DEFAULT_UNIT,
+                Type.SUMMARY,
+                Collections.<LabelKey>emptyList()));
+    assertThat(metrics.get(7).getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metrics.get(7).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+    assertThat(metrics.get(7).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metrics.get(7).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(
+            Value.summaryValue(
+                Summary.create(
+                    1L,
+                    0.0,
+                    Snapshot.create(
+                        1L,
+                        0.0,
+                        Arrays.asList(
+                            ValueAtPercentile.create(50.0, 200.0),
+                            ValueAtPercentile.create(75.0, 200.0),
+                            ValueAtPercentile.create(98.0, 200.0),
+                            ValueAtPercentile.create(99.0, 200.0),
+                            ValueAtPercentile.create(99.9, 200.0))))));
+    assertThat(metrics.get(7).getTimeSeriesList().get(0).getStartTimestamp())
+        .isInstanceOf(Timestamp.class);
+
+    assertThat(metrics.get(8).getMetricDescriptor())
+        .isEqualTo(
+            MetricDescriptor.create(
+                "codahale_requests_timer",
+                "Collected from codahale (metric=requests, " + "type=com.codahale.metrics.Timer)",
+                DEFAULT_UNIT,
+                Type.SUMMARY,
+                Collections.<LabelKey>emptyList()));
+    assertThat(metrics.get(8).getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metrics.get(8).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+    assertThat(metrics.get(8).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metrics.get(8).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(
+            Value.summaryValue(
+                Summary.create(
+                    1L,
+                    0.0,
+                    Snapshot.create(
+                        1L,
+                        0.0,
+                        Arrays.asList(
+                            ValueAtPercentile.create(50.0, timer.getSnapshot().getMedian()),
+                            ValueAtPercentile.create(75.0, timer.getSnapshot().get75thPercentile()),
+                            ValueAtPercentile.create(98.0, timer.getSnapshot().get98thPercentile()),
+                            ValueAtPercentile.create(99.0, timer.getSnapshot().get99thPercentile()),
+                            ValueAtPercentile.create(
+                                99.9, timer.getSnapshot().get999thPercentile()))))));
+
+    assertThat(metrics.get(8).getTimeSeriesList().get(0).getStartTimestamp())
+        .isInstanceOf(Timestamp.class);
+  }
+
+  @Test
+  public void empty_GetMetrics() {
+    assertThat(dropWizardMetrics.getMetrics()).isEmpty();
+  }
+}
diff --git a/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardUtilsTest.java b/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardUtilsTest.java
new file mode 100644
index 0000000..4dd27f2
--- /dev/null
+++ b/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardUtilsTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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.dropwizard;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.codahale.metrics.Counter;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link DropWizardUtils}. */
+@RunWith(JUnit4.class)
+public class DropWizardUtilsTest {
+
+  @Test
+  public void generateFullMetricName() {
+    assertThat(DropWizardUtils.generateFullMetricName("requests", "gauge"))
+        .isEqualTo("codahale_requests_gauge");
+  }
+
+  @Test
+  public void generateFullMetricDescription() {
+    assertThat(DropWizardUtils.generateFullMetricDescription("Counter", new Counter()))
+        .isEqualTo("Collected from codahale (metric=Counter, type=com.codahale.metrics.Counter)");
+  }
+}
diff --git a/contrib/exemplar_util/README.md b/contrib/exemplar_util/README.md
new file mode 100644
index 0000000..1c9d62d
--- /dev/null
+++ b/contrib/exemplar_util/README.md
@@ -0,0 +1,36 @@
+# OpenCensus Exemplar Util
+
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Exemplar Util for Java* is a collection of utilities for recording Exemplars for 
+OpenCensus stats.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-exemplar-util</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-exemplar-util:0.16.1'
+```
+
+[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-exemplar-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-exemplar-util
+
diff --git a/contrib/exemplar_util/build.gradle b/contrib/exemplar_util/build.gradle
new file mode 100644
index 0000000..9404b87
--- /dev/null
+++ b/contrib/exemplar_util/build.gradle
@@ -0,0 +1,15 @@
+description = 'OpenCensus Exemplar Util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+dependencies {
+    compile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/exemplar_util/src/main/java/io/opencensus/contrib/exemplar/util/ExemplarUtils.java b/contrib/exemplar_util/src/main/java/io/opencensus/contrib/exemplar/util/ExemplarUtils.java
new file mode 100644
index 0000000..7eb2116
--- /dev/null
+++ b/contrib/exemplar_util/src/main/java/io/opencensus/contrib/exemplar/util/ExemplarUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018, 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.exemplar.util;
+
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import javax.annotation.Nullable;
+
+/**
+ * Utils for recording {@link Exemplar}s for OpenCensus stats.
+ *
+ * @since 0.16
+ */
+public final class ExemplarUtils {
+
+  /**
+   * Key for {@link TraceId} in the contextual information of an {@link Exemplar}.
+   *
+   * <p>For the {@code TraceId} value of this key, it is suggested to encode it in hex (base 16)
+   * lower case.
+   *
+   * @since 0.16
+   */
+  public static final String ATTACHMENT_KEY_TRACE_ID = "TraceId";
+
+  /**
+   * Key for {@link SpanId} in the contextual information of an {@link Exemplar}.
+   *
+   * <p>For the {@code SpanId} value of this key, it is suggested to encode it in hex (base 16)
+   * lower case.
+   *
+   * @since 0.16
+   */
+  public static final String ATTACHMENT_KEY_SPAN_ID = "SpanId";
+
+  /**
+   * Puts a {@link SpanContext} into the attachments of the given {@link MeasureMap}.
+   *
+   * <p>{@link TraceId} and {@link SpanId} of the {@link SpanContext} will be encoded in base 16
+   * lower case encoding.
+   *
+   * @param measureMap the {@code MeasureMap}
+   * @param spanContext the {@code SpanContext} to be put as attachments.
+   * @since 0.16
+   */
+  public static void putSpanContextAttachments(MeasureMap measureMap, SpanContext spanContext) {
+    checkNotNull(measureMap, "measureMap");
+    checkNotNull(spanContext, "spanContext");
+    measureMap.putAttachment(ATTACHMENT_KEY_TRACE_ID, spanContext.getTraceId().toLowerBase16());
+    measureMap.putAttachment(ATTACHMENT_KEY_SPAN_ID, spanContext.getSpanId().toLowerBase16());
+  }
+
+  // TODO: reuse this method from shared artifact.
+  private static <T> T checkNotNull(T reference, @Nullable Object errorMessage) {
+    if (reference == null) {
+      throw new NullPointerException(String.valueOf(errorMessage));
+    }
+    return reference;
+  }
+
+  private ExemplarUtils() {}
+}
diff --git a/contrib/exemplar_util/src/test/java/io/opencensus/contrib/exemplar/util/ExemplarUtilsTest.java b/contrib/exemplar_util/src/test/java/io/opencensus/contrib/exemplar/util/ExemplarUtilsTest.java
new file mode 100644
index 0000000..766f2c4
--- /dev/null
+++ b/contrib/exemplar_util/src/test/java/io/opencensus/contrib/exemplar/util/ExemplarUtilsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2018, 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.exemplar.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.exemplar.util.ExemplarUtils.ATTACHMENT_KEY_SPAN_ID;
+import static io.opencensus.contrib.exemplar.util.ExemplarUtils.ATTACHMENT_KEY_TRACE_ID;
+
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.tags.TagContext;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ExemplarUtils}. */
+@RunWith(JUnit4.class)
+public class ExemplarUtilsTest {
+
+  private static final Random RANDOM = new Random(1234);
+  private static final TraceId TRACE_ID = TraceId.generateRandomId(RANDOM);
+  private static final SpanId SPAN_ID = SpanId.generateRandomId(RANDOM);
+  private static final SpanContext SPAN_CONTEXT =
+      SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT);
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void putSpanContext() {
+    FakeMeasureMap measureMap = new FakeMeasureMap();
+    ExemplarUtils.putSpanContextAttachments(measureMap, SPAN_CONTEXT);
+    assertThat(measureMap.attachments)
+        .containsExactly(
+            ATTACHMENT_KEY_TRACE_ID,
+            TRACE_ID.toLowerBase16(),
+            ATTACHMENT_KEY_SPAN_ID,
+            SPAN_ID.toLowerBase16());
+  }
+
+  @Test
+  public void putSpanContext_PreventNullMeasureMap() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("measureMap");
+    ExemplarUtils.putSpanContextAttachments(null, SPAN_CONTEXT);
+  }
+
+  @Test
+  public void putSpanContext_PreventNullSpanContext() {
+    FakeMeasureMap measureMap = new FakeMeasureMap();
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("spanContext");
+    ExemplarUtils.putSpanContextAttachments(measureMap, null);
+  }
+
+  private static final class FakeMeasureMap extends MeasureMap {
+
+    private final Map<String, String> attachments = new HashMap<String, String>();
+
+    @Override
+    public MeasureMap putAttachment(String key, String value) {
+      attachments.put(key, value);
+      return this;
+    }
+
+    @Override
+    public MeasureMap put(MeasureDouble measure, double value) {
+      return this;
+    }
+
+    @Override
+    public MeasureMap put(MeasureLong measure, long value) {
+      return this;
+    }
+
+    @Override
+    public void record() {}
+
+    @Override
+    public void record(TagContext tags) {}
+  }
+}
diff --git a/contrib/grpc_metrics/README.md b/contrib/grpc_metrics/README.md
new file mode 100644
index 0000000..b80cee2
--- /dev/null
+++ b/contrib/grpc_metrics/README.md
@@ -0,0 +1,5 @@
+# OpenCensus gRPC Metrics
+
+RPC measure and view constants used by gRPC.
+
+This library may be moved into gRPC in the future.
diff --git a/contrib/grpc_metrics/build.gradle b/contrib/grpc_metrics/build.gradle
new file mode 100644
index 0000000..a2de78d
--- /dev/null
+++ b/contrib/grpc_metrics/build.gradle
@@ -0,0 +1,16 @@
+description = 'OpenCensus gRPC Metrics'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.guava
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstants.java b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstants.java
new file mode 100644
index 0000000..c09cfbf
--- /dev/null
+++ b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstants.java
@@ -0,0 +1,495 @@
+/*
+ * 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.grpc.metrics;
+
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagKey;
+
+/**
+ * Constants for collecting rpc stats.
+ *
+ * @since 0.8
+ */
+public final class RpcMeasureConstants {
+
+  /**
+   * Tag key that represents a gRPC canonical status. Refer to
+   * https://github.com/grpc/grpc/blob/master/doc/statuscodes.md.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_STATUS} and {@link #GRPC_SERVER_STATUS}.
+   */
+  @Deprecated public static final TagKey RPC_STATUS = TagKey.create("canonical_status");
+
+  /**
+   * Tag key that represents a gRPC method.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_METHOD} and {@link #GRPC_SERVER_METHOD}.
+   */
+  @Deprecated public static final TagKey RPC_METHOD = TagKey.create("method");
+
+  /**
+   * Tag key that represents a client gRPC canonical status. Refer to
+   * https://github.com/grpc/grpc/blob/master/doc/statuscodes.md.
+   *
+   * <p>{@link #GRPC_CLIENT_STATUS} is set when an outgoing request finishes and is only available
+   * around metrics recorded at the end of the outgoing request.
+   *
+   * @since 0.13
+   */
+  public static final TagKey GRPC_CLIENT_STATUS = TagKey.create("grpc_client_status");
+
+  /**
+   * Tag key that represents a server gRPC canonical status. Refer to
+   * https://github.com/grpc/grpc/blob/master/doc/statuscodes.md.
+   *
+   * <p>{@link #GRPC_SERVER_STATUS} is set when an incoming request finishes and is only available
+   * around metrics recorded at the end of the incoming request.
+   *
+   * @since 0.13
+   */
+  public static final TagKey GRPC_SERVER_STATUS = TagKey.create("grpc_server_status");
+
+  /**
+   * Tag key that represents a client gRPC method.
+   *
+   * <p>{@link #GRPC_CLIENT_METHOD} is set when an outgoing request starts and is available in all
+   * the recorded metrics.
+   *
+   * @since 0.13
+   */
+  public static final TagKey GRPC_CLIENT_METHOD = TagKey.create("grpc_client_method");
+
+  /**
+   * Tag key that represents a server gRPC method.
+   *
+   * <p>{@link #GRPC_SERVER_METHOD} is set when an incoming request starts and is available in the
+   * context for the entire RPC call handling.
+   *
+   * @since 0.13
+   */
+  public static final TagKey GRPC_SERVER_METHOD = TagKey.create("grpc_server_method");
+
+  // Constants used to define the following Measures.
+
+  /**
+   * Unit string for byte.
+   *
+   * @since 0.8
+   */
+  private static final String BYTE = "By";
+
+  /**
+   * Unit string for count.
+   *
+   * @since 0.8
+   */
+  private static final String COUNT = "1";
+
+  /**
+   * Unit string for millisecond.
+   *
+   * @since 0.8
+   */
+  private static final String MILLISECOND = "ms";
+
+  // RPC client Measures.
+
+  /**
+   * {@link Measure} for gRPC client error counts.
+   *
+   * @since 0.8
+   * @deprecated because error counts can be computed on your metrics backend by totalling the
+   *     different per-status values.
+   */
+  @Deprecated
+  public static final MeasureLong RPC_CLIENT_ERROR_COUNT =
+      Measure.MeasureLong.create("grpc.io/client/error_count", "RPC Errors", COUNT);
+
+  /**
+   * {@link Measure} for gRPC client request bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_SENT_BYTES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_CLIENT_REQUEST_BYTES =
+      Measure.MeasureDouble.create("grpc.io/client/request_bytes", "Request bytes", BYTE);
+
+  /**
+   * {@link Measure} for gRPC client response bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_BYTES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_CLIENT_RESPONSE_BYTES =
+      Measure.MeasureDouble.create("grpc.io/client/response_bytes", "Response bytes", BYTE);
+
+  /**
+   * {@link Measure} for gRPC client roundtrip latency in milliseconds.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_ROUNDTRIP_LATENCY}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_CLIENT_ROUNDTRIP_LATENCY =
+      Measure.MeasureDouble.create(
+          "grpc.io/client/roundtrip_latency", "RPC roundtrip latency msec", MILLISECOND);
+
+  /**
+   * {@link Measure} for gRPC client server elapsed time in milliseconds.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_SERVER_LATENCY}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_CLIENT_SERVER_ELAPSED_TIME =
+      Measure.MeasureDouble.create(
+          "grpc.io/client/server_elapsed_time", "Server elapsed time in msecs", MILLISECOND);
+
+  /**
+   * {@link Measure} for gRPC client uncompressed request bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_SENT_BYTES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES =
+      Measure.MeasureDouble.create(
+          "grpc.io/client/uncompressed_request_bytes", "Uncompressed Request bytes", BYTE);
+
+  /**
+   * {@link Measure} for gRPC client uncompressed response bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_BYTES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES =
+      Measure.MeasureDouble.create(
+          "grpc.io/client/uncompressed_response_bytes", "Uncompressed Response bytes", BYTE);
+
+  /**
+   * {@link Measure} for number of started client RPCs.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_STARTED_RPCS}.
+   */
+  @Deprecated
+  public static final MeasureLong RPC_CLIENT_STARTED_COUNT =
+      Measure.MeasureLong.create(
+          "grpc.io/client/started_count", "Number of client RPCs (streams) started", COUNT);
+
+  /**
+   * {@link Measure} for number of finished client RPCs.
+   *
+   * @since 0.8
+   * @deprecated since finished count can be inferred with a {@code Count} aggregation on {@link
+   *     #GRPC_CLIENT_SERVER_LATENCY}.
+   */
+  @Deprecated
+  public static final MeasureLong RPC_CLIENT_FINISHED_COUNT =
+      Measure.MeasureLong.create(
+          "grpc.io/client/finished_count", "Number of client RPCs (streams) finished", COUNT);
+
+  /**
+   * {@link Measure} for client RPC request message counts.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_SENT_MESSAGES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureLong RPC_CLIENT_REQUEST_COUNT =
+      Measure.MeasureLong.create(
+          "grpc.io/client/request_count", "Number of client RPC request messages", COUNT);
+
+  /**
+   * {@link Measure} for client RPC response message counts.
+   *
+   * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC}.
+   * @since 0.8
+   */
+  @Deprecated
+  public static final MeasureLong RPC_CLIENT_RESPONSE_COUNT =
+      Measure.MeasureLong.create(
+          "grpc.io/client/response_count", "Number of client RPC response messages", COUNT);
+
+  /**
+   * {@link Measure} for total bytes sent across all request messages per RPC.
+   *
+   * @since 0.13
+   */
+  public static final MeasureDouble GRPC_CLIENT_SENT_BYTES_PER_RPC =
+      Measure.MeasureDouble.create(
+          "grpc.io/client/sent_bytes_per_rpc",
+          "Total bytes sent across all request messages per RPC",
+          BYTE);
+
+  /**
+   * {@link Measure} for total bytes received across all response messages per RPC.
+   *
+   * @since 0.13
+   */
+  public static final MeasureDouble GRPC_CLIENT_RECEIVED_BYTES_PER_RPC =
+      Measure.MeasureDouble.create(
+          "grpc.io/client/received_bytes_per_rpc",
+          "Total bytes received across all response messages per RPC",
+          BYTE);
+
+  /**
+   * {@link Measure} for gRPC client roundtrip latency in milliseconds.
+   *
+   * @since 0.13
+   */
+  public static final MeasureDouble GRPC_CLIENT_ROUNDTRIP_LATENCY =
+      Measure.MeasureDouble.create(
+          "grpc.io/client/roundtrip_latency",
+          "Time between first byte of request sent to last byte of response received, "
+              + "or terminal error.",
+          MILLISECOND);
+
+  /**
+   * {@link Measure} for number of messages sent in the RPC.
+   *
+   * @since 0.13
+   */
+  public static final MeasureLong GRPC_CLIENT_SENT_MESSAGES_PER_RPC =
+      Measure.MeasureLong.create(
+          "grpc.io/client/sent_messages_per_rpc", "Number of messages sent in the RPC", COUNT);
+
+  /**
+   * {@link Measure} for number of response messages received per RPC.
+   *
+   * @since 0.13
+   */
+  public static final MeasureLong GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC =
+      Measure.MeasureLong.create(
+          "grpc.io/client/received_messages_per_rpc",
+          "Number of response messages received per RPC",
+          COUNT);
+
+  /**
+   * {@link Measure} for gRPC server latency in milliseconds.
+   *
+   * @since 0.13
+   */
+  public static final MeasureDouble GRPC_CLIENT_SERVER_LATENCY =
+      Measure.MeasureDouble.create(
+          "grpc.io/client/server_latency", "Server latency in msecs", MILLISECOND);
+
+  /**
+   * {@link Measure} for total number of client RPCs ever opened, including those that have not
+   * completed.
+   *
+   * @since 0.14
+   */
+  public static final MeasureLong GRPC_CLIENT_STARTED_RPCS =
+      Measure.MeasureLong.create(
+          "grpc.io/client/started_rpcs", "Number of started client RPCs.", COUNT);
+
+  // RPC server Measures.
+
+  /**
+   * {@link Measure} for gRPC server error counts.
+   *
+   * @since 0.8
+   * @deprecated because error counts can be computed on your metrics backend by totalling the
+   *     different per-status values.
+   */
+  @Deprecated
+  public static final MeasureLong RPC_SERVER_ERROR_COUNT =
+      Measure.MeasureLong.create("grpc.io/server/error_count", "RPC Errors", COUNT);
+
+  /**
+   * {@link Measure} for gRPC server request bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_BYTES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_SERVER_REQUEST_BYTES =
+      Measure.MeasureDouble.create("grpc.io/server/request_bytes", "Request bytes", BYTE);
+
+  /**
+   * {@link Measure} for gRPC server response bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SENT_BYTES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_SERVER_RESPONSE_BYTES =
+      Measure.MeasureDouble.create("grpc.io/server/response_bytes", "Response bytes", BYTE);
+
+  /**
+   * {@link Measure} for gRPC server elapsed time in milliseconds.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SERVER_LATENCY}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_SERVER_SERVER_ELAPSED_TIME =
+      Measure.MeasureDouble.create(
+          "grpc.io/server/server_elapsed_time", "Server elapsed time in msecs", MILLISECOND);
+
+  /**
+   * {@link Measure} for gRPC server latency in milliseconds.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SERVER_LATENCY}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_SERVER_SERVER_LATENCY =
+      Measure.MeasureDouble.create(
+          "grpc.io/server/server_latency", "Latency in msecs", MILLISECOND);
+
+  /**
+   * {@link Measure} for gRPC server uncompressed request bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_BYTES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES =
+      Measure.MeasureDouble.create(
+          "grpc.io/server/uncompressed_request_bytes", "Uncompressed Request bytes", BYTE);
+
+  /**
+   * {@link Measure} for gRPC server uncompressed response bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SENT_BYTES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureDouble RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES =
+      Measure.MeasureDouble.create(
+          "grpc.io/server/uncompressed_response_bytes", "Uncompressed Response bytes", BYTE);
+
+  /**
+   * {@link Measure} for number of started server RPCs.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_STARTED_RPCS}.
+   */
+  @Deprecated
+  public static final MeasureLong RPC_SERVER_STARTED_COUNT =
+      Measure.MeasureLong.create(
+          "grpc.io/server/started_count", "Number of server RPCs (streams) started", COUNT);
+
+  /**
+   * {@link Measure} for number of finished server RPCs.
+   *
+   * @since 0.8
+   * @deprecated since finished count can be inferred with a {@code Count} aggregation on {@link
+   *     #GRPC_SERVER_SERVER_LATENCY}.
+   */
+  @Deprecated
+  public static final MeasureLong RPC_SERVER_FINISHED_COUNT =
+      Measure.MeasureLong.create(
+          "grpc.io/server/finished_count", "Number of server RPCs (streams) finished", COUNT);
+
+  /**
+   * {@link Measure} for server RPC request message counts.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureLong RPC_SERVER_REQUEST_COUNT =
+      Measure.MeasureLong.create(
+          "grpc.io/server/request_count", "Number of server RPC request messages", COUNT);
+
+  /**
+   * {@link Measure} for server RPC response message counts.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SENT_MESSAGES_PER_RPC}.
+   */
+  @Deprecated
+  public static final MeasureLong RPC_SERVER_RESPONSE_COUNT =
+      Measure.MeasureLong.create(
+          "grpc.io/server/response_count", "Number of server RPC response messages", COUNT);
+
+  /**
+   * {@link Measure} for total bytes sent across all response messages per RPC.
+   *
+   * @since 0.13
+   */
+  public static final MeasureDouble GRPC_SERVER_SENT_BYTES_PER_RPC =
+      Measure.MeasureDouble.create(
+          "grpc.io/server/sent_bytes_per_rpc",
+          "Total bytes sent across all response messages per RPC",
+          BYTE);
+
+  /**
+   * {@link Measure} for total bytes received across all messages per RPC.
+   *
+   * @since 0.13
+   */
+  public static final MeasureDouble GRPC_SERVER_RECEIVED_BYTES_PER_RPC =
+      Measure.MeasureDouble.create(
+          "grpc.io/server/received_bytes_per_rpc",
+          "Total bytes received across all messages per RPC",
+          BYTE);
+
+  /**
+   * {@link Measure} for number of messages sent in each RPC.
+   *
+   * @since 0.13
+   */
+  public static final MeasureLong GRPC_SERVER_SENT_MESSAGES_PER_RPC =
+      Measure.MeasureLong.create(
+          "grpc.io/server/sent_messages_per_rpc", "Number of messages sent in each RPC", COUNT);
+
+  /**
+   * {@link Measure} for number of messages received in each RPC.
+   *
+   * @since 0.13
+   */
+  public static final MeasureLong GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC =
+      Measure.MeasureLong.create(
+          "grpc.io/server/received_messages_per_rpc",
+          "Number of messages received in each RPC",
+          COUNT);
+
+  /**
+   * {@link Measure} for gRPC server latency in milliseconds.
+   *
+   * @since 0.13
+   */
+  public static final MeasureDouble GRPC_SERVER_SERVER_LATENCY =
+      Measure.MeasureDouble.create(
+          "grpc.io/server/server_latency",
+          "Time between first byte of request received to last byte of response sent, "
+              + "or terminal error.",
+          MILLISECOND);
+
+  /**
+   * {@link Measure} for total number of server RPCs ever opened, including those that have not
+   * completed.
+   *
+   * @since 0.14
+   */
+  public static final MeasureLong GRPC_SERVER_STARTED_RPCS =
+      Measure.MeasureLong.create(
+          "grpc.io/server/started_rpcs", "Number of started server RPCs.", COUNT);
+
+  private RpcMeasureConstants() {}
+}
diff --git a/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViewConstants.java b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViewConstants.java
new file mode 100644
index 0000000..fbe1d58
--- /dev/null
+++ b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViewConstants.java
@@ -0,0 +1,1339 @@
+/*
+ * 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.grpc.metrics;
+
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_METHOD;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_RECEIVED_BYTES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_SENT_BYTES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_SENT_MESSAGES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_SERVER_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_STARTED_RPCS;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_STATUS;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_METHOD;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_RECEIVED_BYTES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_SENT_BYTES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_SENT_MESSAGES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_SERVER_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_STARTED_RPCS;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_STATUS;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_ERROR_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_FINISHED_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_REQUEST_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_RESPONSE_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_RESPONSE_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_ROUNDTRIP_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_SERVER_ELAPSED_TIME;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_STARTED_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_METHOD;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_ERROR_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_FINISHED_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_REQUEST_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_RESPONSE_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_RESPONSE_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_SERVER_ELAPSED_TIME;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_SERVER_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_STARTED_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_STATUS;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.View;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Constants for exporting views on rpc stats.
+ *
+ * @since 0.8
+ */
+@SuppressWarnings("deprecation")
+public final class RpcViewConstants {
+
+  // Common histogram bucket boundaries for bytes received/sets Views.
+  @VisibleForTesting
+  static final List<Double> RPC_BYTES_BUCKET_BOUNDARIES =
+      Collections.unmodifiableList(
+          Arrays.asList(
+              0.0,
+              1024.0,
+              2048.0,
+              4096.0,
+              16384.0,
+              65536.0,
+              262144.0,
+              1048576.0,
+              4194304.0,
+              16777216.0,
+              67108864.0,
+              268435456.0,
+              1073741824.0,
+              4294967296.0));
+
+  // Common histogram bucket boundaries for latency and elapsed-time Views.
+  @VisibleForTesting
+  static final List<Double> RPC_MILLIS_BUCKET_BOUNDARIES =
+      Collections.unmodifiableList(
+          Arrays.asList(
+              0.0, 0.01, 0.05, 0.1, 0.3, 0.6, 0.8, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0,
+              16.0, 20.0, 25.0, 30.0, 40.0, 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0,
+              300.0, 400.0, 500.0, 650.0, 800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0,
+              100000.0));
+
+  static final List<Double> RPC_MILLIS_BUCKET_BOUNDARIES_DEPRECATED =
+      Collections.unmodifiableList(
+          Arrays.asList(
+              0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0, 25.0, 30.0, 40.0,
+              50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0, 300.0, 400.0, 500.0, 650.0,
+              800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0, 100000.0));
+
+  // Common histogram bucket boundaries for request/response count Views.
+  @VisibleForTesting
+  static final List<Double> RPC_COUNT_BUCKET_BOUNDARIES =
+      Collections.unmodifiableList(
+          Arrays.asList(
+              0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, 1024.0, 2048.0,
+              4096.0, 8192.0, 16384.0, 32768.0, 65536.0));
+
+  // Use Aggregation.Mean to record sum and count stats at the same time.
+  @VisibleForTesting static final Aggregation MEAN = Aggregation.Mean.create();
+  @VisibleForTesting static final Aggregation COUNT = Count.create();
+
+  @VisibleForTesting
+  static final Aggregation AGGREGATION_WITH_BYTES_HISTOGRAM =
+      Distribution.create(BucketBoundaries.create(RPC_BYTES_BUCKET_BOUNDARIES));
+
+  @VisibleForTesting
+  static final Aggregation AGGREGATION_WITH_MILLIS_HISTOGRAM =
+      Distribution.create(BucketBoundaries.create(RPC_MILLIS_BUCKET_BOUNDARIES));
+
+  static final Aggregation AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED =
+      Distribution.create(BucketBoundaries.create(RPC_MILLIS_BUCKET_BOUNDARIES_DEPRECATED));
+
+  @VisibleForTesting
+  static final Aggregation AGGREGATION_WITH_COUNT_HISTOGRAM =
+      Distribution.create(BucketBoundaries.create(RPC_COUNT_BUCKET_BOUNDARIES));
+
+  @VisibleForTesting static final Duration MINUTE = Duration.create(60, 0);
+  @VisibleForTesting static final Duration HOUR = Duration.create(60 * 60, 0);
+
+  @VisibleForTesting
+  static final View.AggregationWindow CUMULATIVE = View.AggregationWindow.Cumulative.create();
+
+  @VisibleForTesting
+  static final View.AggregationWindow INTERVAL_MINUTE =
+      View.AggregationWindow.Interval.create(MINUTE);
+
+  @VisibleForTesting
+  static final View.AggregationWindow INTERVAL_HOUR = View.AggregationWindow.Interval.create(HOUR);
+
+  // Rpc client cumulative views.
+
+  /**
+   * Cumulative {@link View} for client RPC errors.
+   *
+   * @since 0.8
+   * @deprecated since error count measure is deprecated.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_ERROR_COUNT_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/error_count/cumulative"),
+          "RPC Errors",
+          RPC_CLIENT_ERROR_COUNT,
+          MEAN,
+          Arrays.asList(RPC_STATUS, RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for client roundtrip latency in milliseconds.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/roundtrip_latency/cumulative"),
+          "Latency in msecs",
+          RPC_CLIENT_ROUNDTRIP_LATENCY,
+          AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for client server elapsed time in milliseconds.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_SERVER_LATENCY_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_SERVER_ELAPSED_TIME_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/server_elapsed_time/cumulative"),
+          "Server elapsed time in msecs",
+          RPC_CLIENT_SERVER_ELAPSED_TIME,
+          AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for client request bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_REQUEST_BYTES_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/request_bytes/cumulative"),
+          "Request bytes",
+          RPC_CLIENT_REQUEST_BYTES,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for client response bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_RESPONSE_BYTES_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/response_bytes/cumulative"),
+          "Response bytes",
+          RPC_CLIENT_RESPONSE_BYTES,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for client uncompressed request bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/uncompressed_request_bytes/cumulative"),
+          "Uncompressed Request bytes",
+          RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for client uncompressed response bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/uncompressed_response_bytes/cumulative"),
+          "Uncompressed Response bytes",
+          RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for client request message counts.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_SENT_MESSAGES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_REQUEST_COUNT_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/request_count/cumulative"),
+          "Count of request messages per client RPC",
+          RPC_CLIENT_REQUEST_COUNT,
+          AGGREGATION_WITH_COUNT_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for client response message counts.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_RESPONSE_COUNT_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/response_count/cumulative"),
+          "Count of response messages per client RPC",
+          RPC_CLIENT_RESPONSE_COUNT,
+          AGGREGATION_WITH_COUNT_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for started client RPCs.
+   *
+   * @since 0.12
+   * @deprecated in favor of {@link #GRPC_CLIENT_STARTED_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/started_count/cumulative"),
+          "Number of started client RPCs",
+          RPC_CLIENT_STARTED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for finished client RPCs.
+   *
+   * @since 0.12
+   * @deprecated in favor of {@link #GRPC_CLIENT_COMPLETED_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_CLIENT_FINISHED_COUNT_CUMULATIVE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/finished_count/cumulative"),
+          "Number of finished client RPCs",
+          RPC_CLIENT_FINISHED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * {@link View} for client roundtrip latency in milliseconds.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/roundtrip_latency"),
+          "Latency in msecs",
+          GRPC_CLIENT_ROUNDTRIP_LATENCY,
+          AGGREGATION_WITH_MILLIS_HISTOGRAM,
+          Arrays.asList(GRPC_CLIENT_METHOD));
+
+  /**
+   * {@link View} for client server latency in milliseconds.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_CLIENT_SERVER_LATENCY_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/server_latency"),
+          "Server latency in msecs",
+          GRPC_CLIENT_SERVER_LATENCY,
+          AGGREGATION_WITH_MILLIS_HISTOGRAM,
+          Arrays.asList(GRPC_CLIENT_METHOD));
+
+  /**
+   * {@link View} for client sent bytes per RPC.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/sent_bytes_per_rpc"),
+          "Sent bytes per RPC",
+          GRPC_CLIENT_SENT_BYTES_PER_RPC,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(GRPC_CLIENT_METHOD));
+
+  /**
+   * {@link View} for client received bytes per RPC.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/received_bytes_per_rpc"),
+          "Received bytes per RPC",
+          GRPC_CLIENT_RECEIVED_BYTES_PER_RPC,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(GRPC_CLIENT_METHOD));
+
+  /**
+   * {@link View} for client sent messages per RPC.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_CLIENT_SENT_MESSAGES_PER_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/sent_messages_per_rpc"),
+          "Number of messages sent in the RPC",
+          GRPC_CLIENT_SENT_MESSAGES_PER_RPC,
+          AGGREGATION_WITH_COUNT_HISTOGRAM,
+          Arrays.asList(GRPC_CLIENT_METHOD));
+
+  /**
+   * {@link View} for client received messages per RPC.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/received_messages_per_rpc"),
+          "Number of response messages received per RPC",
+          GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC,
+          AGGREGATION_WITH_COUNT_HISTOGRAM,
+          Arrays.asList(GRPC_CLIENT_METHOD));
+
+  /**
+   * {@link View} for completed client RPCs.
+   *
+   * <p>This {@code View} uses measure {@code GRPC_CLIENT_ROUNDTRIP_LATENCY}, since completed RPCs
+   * can be inferred over any measure recorded once per RPC (since it's just a count aggregation
+   * over the measure). It would be unnecessary to use a separate "count" measure.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_CLIENT_COMPLETED_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/completed_rpcs"),
+          "Number of completed client RPCs",
+          GRPC_CLIENT_ROUNDTRIP_LATENCY,
+          COUNT,
+          Arrays.asList(GRPC_CLIENT_METHOD, GRPC_CLIENT_STATUS));
+
+  /**
+   * {@link View} for started client RPCs.
+   *
+   * @since 0.14
+   */
+  public static final View GRPC_CLIENT_STARTED_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/started_rpcs"),
+          "Number of started client RPCs",
+          GRPC_CLIENT_STARTED_RPCS,
+          COUNT,
+          Arrays.asList(GRPC_CLIENT_METHOD));
+
+  // Rpc server cumulative views.
+
+  /**
+   * Cumulative {@link View} for server RPC errors.
+   *
+   * @since 0.8
+   * @deprecated since error count measure is deprecated.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_ERROR_COUNT_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/error_count/cumulative"),
+          "RPC Errors",
+          RPC_SERVER_ERROR_COUNT,
+          MEAN,
+          Arrays.asList(RPC_STATUS, RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for server latency in milliseconds.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SERVER_LATENCY_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_SERVER_LATENCY_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/server_latency/cumulative"),
+          "Latency in msecs",
+          RPC_SERVER_SERVER_LATENCY,
+          AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for server elapsed time in milliseconds.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SERVER_LATENCY_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_SERVER_ELAPSED_TIME_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/elapsed_time/cumulative"),
+          "Server elapsed time in msecs",
+          RPC_SERVER_SERVER_ELAPSED_TIME,
+          AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for server request bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_REQUEST_BYTES_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/request_bytes/cumulative"),
+          "Request bytes",
+          RPC_SERVER_REQUEST_BYTES,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for server response bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_RESPONSE_BYTES_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/response_bytes/cumulative"),
+          "Response bytes",
+          RPC_SERVER_RESPONSE_BYTES,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for server uncompressed request bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/uncompressed_request_bytes/cumulative"),
+          "Uncompressed Request bytes",
+          RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for server uncompressed response bytes.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/uncompressed_response_bytes/cumulative"),
+          "Uncompressed Response bytes",
+          RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for server request message counts.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_REQUEST_COUNT_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/request_count/cumulative"),
+          "Count of request messages per server RPC",
+          RPC_SERVER_REQUEST_COUNT,
+          AGGREGATION_WITH_COUNT_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for server response message counts.
+   *
+   * @since 0.8
+   * @deprecated in favor of {@link #GRPC_SERVER_SENT_MESSAGES_PER_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_RESPONSE_COUNT_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/response_count/cumulative"),
+          "Count of response messages per server RPC",
+          RPC_SERVER_RESPONSE_COUNT,
+          AGGREGATION_WITH_COUNT_HISTOGRAM,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for started server RPCs.
+   *
+   * @since 0.12
+   * @deprecated in favor of {@link #GRPC_SERVER_STARTED_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/started_count/cumulative"),
+          "Number of started server RPCs",
+          RPC_SERVER_STARTED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * Cumulative {@link View} for finished server RPCs.
+   *
+   * @since 0.12
+   * @deprecated in favor of {@link #GRPC_SERVER_COMPLETED_RPC_VIEW}.
+   */
+  @Deprecated
+  public static final View RPC_SERVER_FINISHED_COUNT_CUMULATIVE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/finished_count/cumulative"),
+          "Number of finished server RPCs",
+          RPC_SERVER_FINISHED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          CUMULATIVE);
+
+  /**
+   * {@link View} for server server latency in milliseconds.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_SERVER_SERVER_LATENCY_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/server_latency"),
+          "Server latency in msecs",
+          GRPC_SERVER_SERVER_LATENCY,
+          AGGREGATION_WITH_MILLIS_HISTOGRAM,
+          Arrays.asList(GRPC_SERVER_METHOD));
+
+  /**
+   * {@link View} for server sent bytes per RPC.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/sent_bytes_per_rpc"),
+          "Sent bytes per RPC",
+          GRPC_SERVER_SENT_BYTES_PER_RPC,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(GRPC_SERVER_METHOD));
+
+  /**
+   * {@link View} for server received bytes per RPC.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/received_bytes_per_rpc"),
+          "Received bytes per RPC",
+          GRPC_SERVER_RECEIVED_BYTES_PER_RPC,
+          AGGREGATION_WITH_BYTES_HISTOGRAM,
+          Arrays.asList(GRPC_SERVER_METHOD));
+
+  /**
+   * {@link View} for server sent messages per RPC.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_SERVER_SENT_MESSAGES_PER_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/sent_messages_per_rpc"),
+          "Number of messages sent in each RPC",
+          GRPC_SERVER_SENT_MESSAGES_PER_RPC,
+          AGGREGATION_WITH_COUNT_HISTOGRAM,
+          Arrays.asList(GRPC_SERVER_METHOD));
+
+  /**
+   * {@link View} for server received messages per RPC.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/received_messages_per_rpc"),
+          "Number of response messages received in each RPC",
+          GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC,
+          AGGREGATION_WITH_COUNT_HISTOGRAM,
+          Arrays.asList(GRPC_SERVER_METHOD));
+
+  /**
+   * {@link View} for completed server RPCs.
+   *
+   * <p>This {@code View} uses measure {@code GRPC_SERVER_SERVER_LATENCY}, since completed RPCs can
+   * be inferred over any measure recorded once per RPC (since it's just a count aggregation over
+   * the measure). It would be unnecessary to use a separate "count" measure.
+   *
+   * @since 0.13
+   */
+  public static final View GRPC_SERVER_COMPLETED_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/completed_rpcs"),
+          "Number of completed server RPCs",
+          GRPC_SERVER_SERVER_LATENCY,
+          COUNT,
+          Arrays.asList(GRPC_SERVER_METHOD, GRPC_SERVER_STATUS));
+
+  /**
+   * {@link View} for started server RPCs.
+   *
+   * @since 0.14
+   */
+  public static final View GRPC_SERVER_STARTED_RPC_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/started_rpcs"),
+          "Number of started server RPCs",
+          GRPC_SERVER_STARTED_RPCS,
+          COUNT,
+          Arrays.asList(GRPC_SERVER_METHOD));
+
+  // Interval Stats
+
+  // RPC client interval views.
+
+  /**
+   * Minute {@link View} for client roundtrip latency in milliseconds.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/roundtrip_latency/minute"),
+          "Minute stats for latency in msecs",
+          RPC_CLIENT_ROUNDTRIP_LATENCY,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for client request bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/request_bytes/minute"),
+          "Minute stats for request size in bytes",
+          RPC_CLIENT_REQUEST_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for client response bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/response_bytes/minute"),
+          "Minute stats for response size in bytes",
+          RPC_CLIENT_RESPONSE_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for client RPC errors.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/error_count/minute"),
+          "Minute stats for rpc errors",
+          RPC_CLIENT_ERROR_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for client uncompressed request bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/uncompressed_request_bytes/minute"),
+          "Minute stats for uncompressed request size in bytes",
+          RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for client uncompressed response bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/uncompressed_response_bytes/minute"),
+          "Minute stats for uncompressed response size in bytes",
+          RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for client server elapsed time in milliseconds.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_SERVER_ELAPSED_TIME_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/server_elapsed_time/minute"),
+          "Minute stats for server elapsed time in msecs",
+          RPC_CLIENT_SERVER_ELAPSED_TIME,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for started client RPCs.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/started_count/minute"),
+          "Minute stats on the number of client RPCs started",
+          RPC_CLIENT_STARTED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for finished client RPCs.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/finished_count/minute"),
+          "Minute stats on the number of client RPCs finished",
+          RPC_CLIENT_FINISHED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for client request messages.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/request_count/minute"),
+          "Minute stats on the count of request messages per client RPC",
+          RPC_CLIENT_REQUEST_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for client response messages.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/response_count/minute"),
+          "Minute stats on the count of response messages per client RPC",
+          RPC_CLIENT_RESPONSE_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Hour {@link View} for client roundtrip latency in milliseconds.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/roundtrip_latency/hour"),
+          "Hour stats for latency in msecs",
+          RPC_CLIENT_ROUNDTRIP_LATENCY,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for client request bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/request_bytes/hour"),
+          "Hour stats for request size in bytes",
+          RPC_CLIENT_REQUEST_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for client response bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/response_bytes/hour"),
+          "Hour stats for response size in bytes",
+          RPC_CLIENT_RESPONSE_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for client RPC errors.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_ERROR_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/error_count/hour"),
+          "Hour stats for rpc errors",
+          RPC_CLIENT_ERROR_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for client uncompressed request bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/uncompressed_request_bytes/hour"),
+          "Hour stats for uncompressed request size in bytes",
+          RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for client uncompressed response bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/uncompressed_response_bytes/hour"),
+          "Hour stats for uncompressed response size in bytes",
+          RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for client server elapsed time in milliseconds.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_SERVER_ELAPSED_TIME_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/server_elapsed_time/hour"),
+          "Hour stats for server elapsed time in msecs",
+          RPC_CLIENT_SERVER_ELAPSED_TIME,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for started client RPCs.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_STARTED_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/started_count/hour"),
+          "Hour stats on the number of client RPCs started",
+          RPC_CLIENT_STARTED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for finished client RPCs.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/finished_count/hour"),
+          "Hour stats on the number of client RPCs finished",
+          RPC_CLIENT_FINISHED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for client request messages.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/request_count/hour"),
+          "Hour stats on the count of request messages per client RPC",
+          RPC_CLIENT_REQUEST_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for client response messages.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/client/response_count/hour"),
+          "Hour stats on the count of response messages per client RPC",
+          RPC_CLIENT_RESPONSE_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  // RPC server interval views.
+
+  /**
+   * Minute {@link View} for server latency in milliseconds.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/server_latency/minute"),
+          "Minute stats for server latency in msecs",
+          RPC_SERVER_SERVER_LATENCY,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for server request bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/request_bytes/minute"),
+          "Minute stats for request size in bytes",
+          RPC_SERVER_REQUEST_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for server response bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/response_bytes/minute"),
+          "Minute stats for response size in bytes",
+          RPC_SERVER_RESPONSE_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for server RPC errors.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_ERROR_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/error_count/minute"),
+          "Minute stats for rpc errors",
+          RPC_SERVER_ERROR_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for server uncompressed request bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/uncompressed_request_bytes/minute"),
+          "Minute stats for uncompressed request size in bytes",
+          RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for server uncompressed response bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/uncompressed_response_bytes/minute"),
+          "Minute stats for uncompressed response size in bytes",
+          RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for server elapsed time in milliseconds.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_SERVER_ELAPSED_TIME_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/server_elapsed_time/minute"),
+          "Minute stats for server elapsed time in msecs",
+          RPC_SERVER_SERVER_ELAPSED_TIME,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for started server RPCs.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_STARTED_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/started_count/minute"),
+          "Minute stats on the number of server RPCs started",
+          RPC_SERVER_STARTED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for finished server RPCs.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/finished_count/minute"),
+          "Minute stats on the number of server RPCs finished",
+          RPC_SERVER_FINISHED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for server request messages.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/request_count/minute"),
+          "Minute stats on the count of request messages per server RPC",
+          RPC_SERVER_REQUEST_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Minute {@link View} for server response messages.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/response_count/minute"),
+          "Minute stats on the count of response messages per server RPC",
+          RPC_SERVER_RESPONSE_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_MINUTE);
+
+  /**
+   * Hour {@link View} for server latency in milliseconds.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_SERVER_LATENCY_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/server_latency/hour"),
+          "Hour stats for server latency in msecs",
+          RPC_SERVER_SERVER_LATENCY,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for server request bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_REQUEST_BYTES_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/request_bytes/hour"),
+          "Hour stats for request size in bytes",
+          RPC_SERVER_REQUEST_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for server response bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/response_bytes/hour"),
+          "Hour stats for response size in bytes",
+          RPC_SERVER_RESPONSE_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for server RPC errors.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_ERROR_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/error_count/hour"),
+          "Hour stats for rpc errors",
+          RPC_SERVER_ERROR_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for server uncompressed request bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/uncompressed_request_bytes/hour"),
+          "Hour stats for uncompressed request size in bytes",
+          RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for server uncompressed response bytes.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/uncompressed_response_bytes/hour"),
+          "Hour stats for uncompressed response size in bytes",
+          RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for server elapsed time in milliseconds.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_SERVER_ELAPSED_TIME_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/server_elapsed_time/hour"),
+          "Hour stats for server elapsed time in msecs",
+          RPC_SERVER_SERVER_ELAPSED_TIME,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for started server RPCs.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_STARTED_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/started_count/hour"),
+          "Hour stats on the number of server RPCs started",
+          RPC_SERVER_STARTED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for finished server RPCs.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_FINISHED_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/finished_count/hour"),
+          "Hour stats on the number of server RPCs finished",
+          RPC_SERVER_FINISHED_COUNT,
+          COUNT,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for server request messages.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_REQUEST_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/request_count/hour"),
+          "Hour stats on the count of request messages per server RPC",
+          RPC_SERVER_REQUEST_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  /**
+   * Hour {@link View} for server response messages.
+   *
+   * @since 0.8
+   */
+  public static final View RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW =
+      View.create(
+          View.Name.create("grpc.io/server/response_count/hour"),
+          "Hour stats on the count of response messages per server RPC",
+          RPC_SERVER_RESPONSE_COUNT,
+          MEAN,
+          Arrays.asList(RPC_METHOD),
+          INTERVAL_HOUR);
+
+  private RpcViewConstants() {}
+}
diff --git a/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViews.java b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViews.java
new file mode 100644
index 0000000..ef06ba2
--- /dev/null
+++ b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViews.java
@@ -0,0 +1,250 @@
+/*
+ * 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.grpc.metrics;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewManager;
+
+/**
+ * Helper class that allows users to register rpc views easily.
+ *
+ * @since 0.11
+ */
+@SuppressWarnings("deprecation")
+public final class RpcViews {
+  @VisibleForTesting
+  static final ImmutableSet<View> RPC_CUMULATIVE_VIEWS_SET =
+      ImmutableSet.of(
+          RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW,
+          RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW,
+          RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW,
+          RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_VIEW,
+          RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_VIEW,
+          RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_VIEW,
+          RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW,
+          RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW,
+          RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_VIEW,
+          RpcViewConstants.RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW,
+          RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_CUMULATIVE_VIEW,
+          RpcViewConstants.RPC_SERVER_ERROR_COUNT_VIEW,
+          RpcViewConstants.RPC_SERVER_SERVER_LATENCY_VIEW,
+          RpcViewConstants.RPC_SERVER_SERVER_ELAPSED_TIME_VIEW,
+          RpcViewConstants.RPC_SERVER_REQUEST_BYTES_VIEW,
+          RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_VIEW,
+          RpcViewConstants.RPC_SERVER_REQUEST_COUNT_VIEW,
+          RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_VIEW,
+          RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW,
+          RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW,
+          RpcViewConstants.RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW,
+          RpcViewConstants.RPC_SERVER_FINISHED_COUNT_CUMULATIVE_VIEW);
+
+  @VisibleForTesting
+  static final ImmutableSet<View> GRPC_CLIENT_VIEWS_SET =
+      ImmutableSet.of(
+          RpcViewConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW,
+          RpcViewConstants.GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW,
+          RpcViewConstants.GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW,
+          RpcViewConstants.GRPC_CLIENT_SENT_MESSAGES_PER_RPC_VIEW,
+          RpcViewConstants.GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC_VIEW,
+          RpcViewConstants.GRPC_CLIENT_SERVER_LATENCY_VIEW,
+          RpcViewConstants.GRPC_CLIENT_COMPLETED_RPC_VIEW,
+          RpcViewConstants.GRPC_CLIENT_STARTED_RPC_VIEW);
+
+  @VisibleForTesting
+  static final ImmutableSet<View> GRPC_SERVER_VIEWS_SET =
+      ImmutableSet.of(
+          RpcViewConstants.GRPC_SERVER_SERVER_LATENCY_VIEW,
+          RpcViewConstants.GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW,
+          RpcViewConstants.GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW,
+          RpcViewConstants.GRPC_SERVER_SENT_MESSAGES_PER_RPC_VIEW,
+          RpcViewConstants.GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC_VIEW,
+          RpcViewConstants.GRPC_SERVER_COMPLETED_RPC_VIEW,
+          RpcViewConstants.GRPC_SERVER_STARTED_RPC_VIEW);
+
+  @VisibleForTesting
+  static final ImmutableSet<View> RPC_INTERVAL_VIEWS_SET =
+      ImmutableSet.of(
+          RpcViewConstants.RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_ERROR_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_SERVER_ELAPSED_TIME_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_STARTED_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW,
+          RpcViewConstants.RPC_CLIENT_ERROR_COUNT_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_STARTED_COUNT_HOUR_VIEW,
+          RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_ERROR_COUNT_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_SERVER_LATENCY_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_SERVER_ELAPSED_TIME_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_REQUEST_BYTES_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_REQUEST_COUNT_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_STARTED_COUNT_HOUR_VIEW,
+          RpcViewConstants.RPC_SERVER_FINISHED_COUNT_HOUR_VIEW);
+
+  /**
+   * Registers all standard gRPC views.
+   *
+   * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+   *
+   * <p>This is equivalent with calling {@link #registerClientGrpcViews()} and {@link
+   * #registerServerGrpcViews()}.
+   *
+   * @since 0.13
+   */
+  public static void registerAllGrpcViews() {
+    registerAllGrpcViews(Stats.getViewManager());
+  }
+
+  @VisibleForTesting
+  static void registerAllGrpcViews(ViewManager viewManager) {
+    registerClientGrpcViews(viewManager);
+    registerServerGrpcViews(viewManager);
+  }
+
+  /**
+   * Registers all standard client gRPC views.
+   *
+   * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+   *
+   * @since 0.16
+   */
+  public static void registerClientGrpcViews() {
+    registerClientGrpcViews(Stats.getViewManager());
+  }
+
+  @VisibleForTesting
+  static void registerClientGrpcViews(ViewManager viewManager) {
+    for (View view : GRPC_CLIENT_VIEWS_SET) {
+      viewManager.registerView(view);
+    }
+  }
+
+  /**
+   * Registers all standard server gRPC views.
+   *
+   * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+   *
+   * @since 0.16
+   */
+  public static void registerServerGrpcViews() {
+    registerServerGrpcViews(Stats.getViewManager());
+  }
+
+  @VisibleForTesting
+  static void registerServerGrpcViews(ViewManager viewManager) {
+    for (View view : GRPC_SERVER_VIEWS_SET) {
+      viewManager.registerView(view);
+    }
+  }
+
+  /**
+   * Registers all standard cumulative views.
+   *
+   * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+   *
+   * @since 0.11.0
+   * @deprecated in favor of {@link #registerAllGrpcViews()}. It is likely that there won't be stats
+   *     for the old views, but you may still want to register the old views before they are
+   *     completely removed.
+   */
+  @Deprecated
+  public static void registerAllCumulativeViews() {
+    registerAllCumulativeViews(Stats.getViewManager());
+  }
+
+  @VisibleForTesting
+  static void registerAllCumulativeViews(ViewManager viewManager) {
+    for (View view : RPC_CUMULATIVE_VIEWS_SET) {
+      viewManager.registerView(view);
+    }
+  }
+
+  /**
+   * Registers all standard interval views.
+   *
+   * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+   *
+   * @since 0.11.0
+   * @deprecated because interval window is deprecated. There won't be interval views in the future.
+   */
+  @Deprecated
+  public static void registerAllIntervalViews() {
+    registerAllIntervalViews(Stats.getViewManager());
+  }
+
+  @VisibleForTesting
+  static void registerAllIntervalViews(ViewManager viewManager) {
+    for (View view : RPC_INTERVAL_VIEWS_SET) {
+      viewManager.registerView(view);
+    }
+  }
+
+  /**
+   * Registers all views.
+   *
+   * <p>This is equivalent with calling {@link #registerAllCumulativeViews()} and {@link
+   * #registerAllIntervalViews()}.
+   *
+   * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+   *
+   * @since 0.11.0
+   * @deprecated in favor of {@link #registerAllGrpcViews()}.
+   */
+  @Deprecated
+  public static void registerAllViews() {
+    registerAllViews(Stats.getViewManager());
+  }
+
+  @VisibleForTesting
+  static void registerAllViews(ViewManager viewManager) {
+    registerAllCumulativeViews(viewManager);
+    registerAllIntervalViews(viewManager);
+  }
+
+  private RpcViews() {}
+}
diff --git a/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstantsTest.java b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstantsTest.java
new file mode 100644
index 0000000..107f0fe
--- /dev/null
+++ b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstantsTest.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.grpc.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link RpcMeasureConstants}. */
+@RunWith(JUnit4.class)
+public class RpcMeasureConstantsTest {
+
+  @Test
+  public void testConstants() {
+    assertThat(RpcMeasureConstants.RPC_STATUS).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_METHOD).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_CLIENT_METHOD).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_SERVER_METHOD).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_CLIENT_STATUS).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_SERVER_STATUS).isNotNull();
+
+    // Test client measurement descriptors.
+    assertThat(RpcMeasureConstants.RPC_CLIENT_ERROR_COUNT).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_ROUNDTRIP_LATENCY).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_REQUEST_BYTES).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_RESPONSE_BYTES).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_REQUEST_COUNT).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_RESPONSE_COUNT).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_STARTED_COUNT).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_FINISHED_COUNT).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_CLIENT_SERVER_ELAPSED_TIME).isNotNull();
+
+    assertThat(RpcMeasureConstants.GRPC_CLIENT_SENT_BYTES_PER_RPC).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_CLIENT_SENT_MESSAGES_PER_RPC).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_CLIENT_RECEIVED_BYTES_PER_RPC).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_CLIENT_SERVER_LATENCY).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_CLIENT_STARTED_RPCS).isNotNull();
+
+    // Test server measurement descriptors.
+    assertThat(RpcMeasureConstants.RPC_SERVER_ERROR_COUNT).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_SERVER_REQUEST_BYTES).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_SERVER_RESPONSE_BYTES).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_SERVER_SERVER_LATENCY).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_SERVER_REQUEST_COUNT).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_SERVER_RESPONSE_COUNT).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_SERVER_STARTED_COUNT).isNotNull();
+    assertThat(RpcMeasureConstants.RPC_SERVER_FINISHED_COUNT).isNotNull();
+
+    assertThat(RpcMeasureConstants.GRPC_SERVER_SENT_BYTES_PER_RPC).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_SERVER_SENT_MESSAGES_PER_RPC).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_SERVER_RECEIVED_BYTES_PER_RPC).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_SERVER_SERVER_LATENCY).isNotNull();
+    assertThat(RpcMeasureConstants.GRPC_SERVER_STARTED_RPCS).isNotNull();
+  }
+}
diff --git a/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewConstantsTest.java b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewConstantsTest.java
new file mode 100644
index 0000000..6f8b516
--- /dev/null
+++ b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewConstantsTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.grpc.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link RpcViewConstants}. */
+@RunWith(JUnit4.class)
+public final class RpcViewConstantsTest {
+
+  @Test
+  public void testConstants() {
+
+    // Test bucket boundaries.
+    assertThat(RpcViewConstants.RPC_BYTES_BUCKET_BOUNDARIES)
+        .containsExactly(
+            0.0,
+            1024.0,
+            2048.0,
+            4096.0,
+            16384.0,
+            65536.0,
+            262144.0,
+            1048576.0,
+            4194304.0,
+            16777216.0,
+            67108864.0,
+            268435456.0,
+            1073741824.0,
+            4294967296.0)
+        .inOrder();
+    assertThat(RpcViewConstants.RPC_MILLIS_BUCKET_BOUNDARIES)
+        .containsExactly(
+            0.0, 0.01, 0.05, 0.1, 0.3, 0.6, 0.8, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0,
+            16.0, 20.0, 25.0, 30.0, 40.0, 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0,
+            300.0, 400.0, 500.0, 650.0, 800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0,
+            100000.0)
+        .inOrder();
+    assertThat(RpcViewConstants.RPC_COUNT_BUCKET_BOUNDARIES)
+        .containsExactly(
+            0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, 1024.0, 2048.0, 4096.0,
+            8192.0, 16384.0, 32768.0, 65536.0)
+        .inOrder();
+
+    // Test Aggregations
+    assertThat(RpcViewConstants.MEAN).isEqualTo(Mean.create());
+    assertThat(RpcViewConstants.COUNT).isEqualTo(Count.create());
+    assertThat(RpcViewConstants.AGGREGATION_WITH_BYTES_HISTOGRAM)
+        .isEqualTo(
+            Distribution.create(
+                BucketBoundaries.create(RpcViewConstants.RPC_BYTES_BUCKET_BOUNDARIES)));
+    assertThat(RpcViewConstants.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+        .isEqualTo(
+            Distribution.create(
+                BucketBoundaries.create(RpcViewConstants.RPC_MILLIS_BUCKET_BOUNDARIES)));
+    assertThat(RpcViewConstants.AGGREGATION_WITH_COUNT_HISTOGRAM)
+        .isEqualTo(
+            Distribution.create(
+                BucketBoundaries.create(RpcViewConstants.RPC_COUNT_BUCKET_BOUNDARIES)));
+
+    // Test Duration and Window
+    assertThat(RpcViewConstants.MINUTE).isEqualTo(Duration.create(60, 0));
+    assertThat(RpcViewConstants.HOUR).isEqualTo(Duration.create(60 * 60, 0));
+    assertThat(RpcViewConstants.CUMULATIVE).isEqualTo(Cumulative.create());
+    assertThat(RpcViewConstants.INTERVAL_MINUTE)
+        .isEqualTo(Interval.create(RpcViewConstants.MINUTE));
+    assertThat(RpcViewConstants.INTERVAL_HOUR).isEqualTo(Interval.create(RpcViewConstants.HOUR));
+
+    // Test client distribution view descriptors.
+    assertThat(RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_VIEW).isNotNull();
+
+    assertThat(RpcViewConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_CLIENT_SENT_MESSAGES_PER_RPC_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_CLIENT_SERVER_LATENCY_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_CLIENT_STARTED_RPC_VIEW).isNotNull();
+
+    // Test server distribution view descriptors.
+    assertThat(RpcViewConstants.RPC_SERVER_ERROR_COUNT_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_SERVER_LATENCY_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_REQUEST_BYTES_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_REQUEST_COUNT_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_VIEW).isNotNull();
+
+    assertThat(RpcViewConstants.GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_SERVER_SENT_MESSAGES_PER_RPC_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_SERVER_SERVER_LATENCY_VIEW).isNotNull();
+    assertThat(RpcViewConstants.GRPC_SERVER_STARTED_RPC_VIEW).isNotNull();
+
+    // Test client interval view descriptors.
+    assertThat(RpcViewConstants.RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW).isNotNull();
+
+    assertThat(RpcViewConstants.RPC_CLIENT_ERROR_COUNT_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_STARTED_COUNT_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW).isNotNull();
+
+    // Test server interval view descriptors.
+    assertThat(RpcViewConstants.RPC_SERVER_ERROR_COUNT_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_STARTED_COUNT_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW).isNotNull();
+
+    assertThat(RpcViewConstants.RPC_SERVER_ERROR_COUNT_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_SERVER_LATENCY_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_REQUEST_BYTES_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_STARTED_COUNT_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_FINISHED_COUNT_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_REQUEST_COUNT_HOUR_VIEW).isNotNull();
+    assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW).isNotNull();
+  }
+}
diff --git a/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewsTest.java b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewsTest.java
new file mode 100644
index 0000000..a908629
--- /dev/null
+++ b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewsTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.grpc.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link RpcViews}. */
+@RunWith(JUnit4.class)
+public class RpcViewsTest {
+
+  @Test
+  public void registerCumulative() {
+    FakeViewManager fakeViewManager = new FakeViewManager();
+    RpcViews.registerAllCumulativeViews(fakeViewManager);
+    assertThat(fakeViewManager.getRegisteredViews())
+        .containsExactlyElementsIn(RpcViews.RPC_CUMULATIVE_VIEWS_SET);
+  }
+
+  @Test
+  public void registerInterval() {
+    FakeViewManager fakeViewManager = new FakeViewManager();
+    RpcViews.registerAllIntervalViews(fakeViewManager);
+    assertThat(fakeViewManager.getRegisteredViews())
+        .containsExactlyElementsIn(RpcViews.RPC_INTERVAL_VIEWS_SET);
+  }
+
+  @Test
+  public void registerAll() {
+    FakeViewManager fakeViewManager = new FakeViewManager();
+    RpcViews.registerAllViews(fakeViewManager);
+    assertThat(fakeViewManager.getRegisteredViews())
+        .containsExactlyElementsIn(
+            ImmutableSet.builder()
+                .addAll(RpcViews.RPC_CUMULATIVE_VIEWS_SET)
+                .addAll(RpcViews.RPC_INTERVAL_VIEWS_SET)
+                .build());
+  }
+
+  @Test
+  public void registerAllGrpcViews() {
+    FakeViewManager fakeViewManager = new FakeViewManager();
+    RpcViews.registerAllGrpcViews(fakeViewManager);
+    assertThat(fakeViewManager.getRegisteredViews())
+        .containsExactlyElementsIn(
+            ImmutableSet.builder()
+                .addAll(RpcViews.GRPC_CLIENT_VIEWS_SET)
+                .addAll(RpcViews.GRPC_SERVER_VIEWS_SET)
+                .build());
+  }
+
+  @Test
+  public void registerClientGrpcViews() {
+    FakeViewManager fakeViewManager = new FakeViewManager();
+    RpcViews.registerClientGrpcViews(fakeViewManager);
+    assertThat(fakeViewManager.getRegisteredViews())
+        .containsExactlyElementsIn(RpcViews.GRPC_CLIENT_VIEWS_SET);
+  }
+
+  @Test
+  public void registerServerGrpcViews() {
+    FakeViewManager fakeViewManager = new FakeViewManager();
+    RpcViews.registerServerGrpcViews(fakeViewManager);
+    assertThat(fakeViewManager.getRegisteredViews())
+        .containsExactlyElementsIn(RpcViews.GRPC_SERVER_VIEWS_SET);
+  }
+
+  // TODO(bdrutu): Test with reflection that all defined gRPC views are registered.
+
+  private static final class FakeViewManager extends ViewManager {
+    private final Map<View.Name, View> registeredViews = Maps.newHashMap();
+
+    private FakeViewManager() {}
+
+    @Override
+    public void registerView(View view) {
+      registeredViews.put(view.getName(), view);
+    }
+
+    @Nullable
+    @Override
+    public ViewData getView(View.Name view) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Set<View> getAllExportedViews() {
+      throw new UnsupportedOperationException();
+    }
+
+    private Collection<View> getRegisteredViews() {
+      return registeredViews.values();
+    }
+  }
+}
diff --git a/contrib/grpc_util/README.md b/contrib/grpc_util/README.md
new file mode 100644
index 0000000..7c5c7b9
--- /dev/null
+++ b/contrib/grpc_util/README.md
@@ -0,0 +1,35 @@
+# OpenCensus gRPC Util
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus gRPC Util for Java* is a collection of utilities for trace instrumentation when 
+working with [gRPC][grpc-url].
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-grpc-util</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-grpc-util:0.16.1'
+```
+
+[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-grpc-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-grpc-util
+[grpc-url]: https://github.com/grpc/grpc-java
diff --git a/contrib/grpc_util/build.gradle b/contrib/grpc_util/build.gradle
new file mode 100644
index 0000000..ecc347d
--- /dev/null
+++ b/contrib/grpc_util/build.gradle
@@ -0,0 +1,26 @@
+description = 'OpenCensus gRPC Util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+dependencies {
+    compile project(':opencensus-api')
+
+    compile (libraries.grpc_core) {
+        // Prefer library version.
+        exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
+
+        // Prefer library version.
+        exclude group: 'com.google.code.findbugs', module: 'jsr305'
+
+        // We will always be more up to date.
+        exclude group: 'io.opencensus', module: 'opencensus-api'
+    }
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/grpc_util/src/main/java/io/opencensus/contrib/grpc/util/StatusConverter.java b/contrib/grpc_util/src/main/java/io/opencensus/contrib/grpc/util/StatusConverter.java
new file mode 100644
index 0000000..92b36d4
--- /dev/null
+++ b/contrib/grpc_util/src/main/java/io/opencensus/contrib/grpc/util/StatusConverter.java
@@ -0,0 +1,165 @@
+/*
+ * 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.grpc.util;
+
+/**
+ * Utility class to convert between {@link io.opencensus.trace.Status} and {@link io.grpc.Status}.
+ *
+ * @since 0.6
+ */
+public final class StatusConverter {
+
+  /**
+   * Returns a {@link io.opencensus.trace.Status.CanonicalCode} from a {@link io.grpc.Status.Code}.
+   *
+   * @param grpcCode the given {@code io.grpc.Status.Code}.
+   * @return a {@code io.opencensus.trace.Status.CanonicalCode} from a {@code io.grpc.Status.Code}.
+   * @since 0.6
+   */
+  public static io.opencensus.trace.Status.CanonicalCode fromGrpcCode(
+      io.grpc.Status.Code grpcCode) {
+    return opencensusStatusFromGrpcCode(grpcCode).getCanonicalCode();
+  }
+
+  /**
+   * Returns a {@link io.opencensus.trace.Status} from a {@link io.grpc.Status}.
+   *
+   * @param grpcStatus the given {@code io.grpc.Status}.
+   * @return a {@code io.opencensus.trace.Status} from a {@code io.grpc.Status}.
+   * @since 0.6
+   */
+  public static io.opencensus.trace.Status fromGrpcStatus(io.grpc.Status grpcStatus) {
+    io.opencensus.trace.Status status = opencensusStatusFromGrpcCode(grpcStatus.getCode());
+    String description = grpcStatus.getDescription();
+    if (description != null) {
+      status = status.withDescription(description);
+    }
+    return status;
+  }
+
+  /**
+   * Returns a {@link io.grpc.Status.Code} from a {@link io.opencensus.trace.Status.CanonicalCode}.
+   *
+   * @param opencensusCanonicalCode the given {@code io.opencensus.trace.Status.CanonicalCode}.
+   * @return a {@code io.grpc.Status.Code} from a {@code io.opencensus.trace.Status.CanonicalCode}.
+   * @since 0.6
+   */
+  public static io.grpc.Status.Code toGrpcCode(
+      io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode) {
+    return grpcStatusFromOpencensusCanonicalCode(opencensusCanonicalCode).getCode();
+  }
+
+  /**
+   * Returns a {@link io.grpc.Status} from a {@link io.opencensus.trace.Status}.
+   *
+   * @param opencensusStatus the given {@code io.opencensus.trace.Status}.
+   * @return a {@code io.grpc.Status} from a {@code io.opencensus.trace.Status}.
+   * @since 0.6
+   */
+  public static io.grpc.Status toGrpcStatus(io.opencensus.trace.Status opencensusStatus) {
+    io.grpc.Status status =
+        grpcStatusFromOpencensusCanonicalCode(opencensusStatus.getCanonicalCode());
+    if (opencensusStatus.getDescription() != null) {
+      status = status.withDescription(opencensusStatus.getDescription());
+    }
+    return status;
+  }
+
+  private static io.opencensus.trace.Status opencensusStatusFromGrpcCode(
+      io.grpc.Status.Code grpcCanonicaleCode) {
+    switch (grpcCanonicaleCode) {
+      case OK:
+        return io.opencensus.trace.Status.OK;
+      case CANCELLED:
+        return io.opencensus.trace.Status.CANCELLED;
+      case UNKNOWN:
+        return io.opencensus.trace.Status.UNKNOWN;
+      case INVALID_ARGUMENT:
+        return io.opencensus.trace.Status.INVALID_ARGUMENT;
+      case DEADLINE_EXCEEDED:
+        return io.opencensus.trace.Status.DEADLINE_EXCEEDED;
+      case NOT_FOUND:
+        return io.opencensus.trace.Status.NOT_FOUND;
+      case ALREADY_EXISTS:
+        return io.opencensus.trace.Status.ALREADY_EXISTS;
+      case PERMISSION_DENIED:
+        return io.opencensus.trace.Status.PERMISSION_DENIED;
+      case RESOURCE_EXHAUSTED:
+        return io.opencensus.trace.Status.RESOURCE_EXHAUSTED;
+      case FAILED_PRECONDITION:
+        return io.opencensus.trace.Status.FAILED_PRECONDITION;
+      case ABORTED:
+        return io.opencensus.trace.Status.ABORTED;
+      case OUT_OF_RANGE:
+        return io.opencensus.trace.Status.OUT_OF_RANGE;
+      case UNIMPLEMENTED:
+        return io.opencensus.trace.Status.UNIMPLEMENTED;
+      case INTERNAL:
+        return io.opencensus.trace.Status.INTERNAL;
+      case UNAVAILABLE:
+        return io.opencensus.trace.Status.UNAVAILABLE;
+      case DATA_LOSS:
+        return io.opencensus.trace.Status.DATA_LOSS;
+      case UNAUTHENTICATED:
+        return io.opencensus.trace.Status.UNAUTHENTICATED;
+    }
+    throw new AssertionError("Unhandled status code " + grpcCanonicaleCode);
+  }
+
+  private static io.grpc.Status grpcStatusFromOpencensusCanonicalCode(
+      io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode) {
+    switch (opencensusCanonicalCode) {
+      case OK:
+        return io.grpc.Status.OK;
+      case CANCELLED:
+        return io.grpc.Status.CANCELLED;
+      case UNKNOWN:
+        return io.grpc.Status.UNKNOWN;
+      case INVALID_ARGUMENT:
+        return io.grpc.Status.INVALID_ARGUMENT;
+      case DEADLINE_EXCEEDED:
+        return io.grpc.Status.DEADLINE_EXCEEDED;
+      case NOT_FOUND:
+        return io.grpc.Status.NOT_FOUND;
+      case ALREADY_EXISTS:
+        return io.grpc.Status.ALREADY_EXISTS;
+      case PERMISSION_DENIED:
+        return io.grpc.Status.PERMISSION_DENIED;
+      case RESOURCE_EXHAUSTED:
+        return io.grpc.Status.RESOURCE_EXHAUSTED;
+      case FAILED_PRECONDITION:
+        return io.grpc.Status.FAILED_PRECONDITION;
+      case ABORTED:
+        return io.grpc.Status.ABORTED;
+      case OUT_OF_RANGE:
+        return io.grpc.Status.OUT_OF_RANGE;
+      case UNIMPLEMENTED:
+        return io.grpc.Status.UNIMPLEMENTED;
+      case INTERNAL:
+        return io.grpc.Status.INTERNAL;
+      case UNAVAILABLE:
+        return io.grpc.Status.UNAVAILABLE;
+      case DATA_LOSS:
+        return io.grpc.Status.DATA_LOSS;
+      case UNAUTHENTICATED:
+        return io.grpc.Status.UNAUTHENTICATED;
+    }
+    throw new AssertionError("Unhandled status code " + opencensusCanonicalCode);
+  }
+
+  private StatusConverter() {}
+}
diff --git a/contrib/grpc_util/src/test/java/io/opencensus/contrib/grpc/util/StatusConverterTest.java b/contrib/grpc_util/src/test/java/io/opencensus/contrib/grpc/util/StatusConverterTest.java
new file mode 100644
index 0000000..a6b5e87
--- /dev/null
+++ b/contrib/grpc_util/src/test/java/io/opencensus/contrib/grpc/util/StatusConverterTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.grpc.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StatusConverter}. */
+@RunWith(JUnit4.class)
+public class StatusConverterTest {
+
+  @Test
+  public void convertFromGrpcCode() {
+    for (io.grpc.Status.Code grpcCanonicalCode : io.grpc.Status.Code.values()) {
+      io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode =
+          StatusConverter.fromGrpcCode(grpcCanonicalCode);
+      assertThat(opencensusCanonicalCode.toString()).isEqualTo(grpcCanonicalCode.toString());
+    }
+  }
+
+  @Test
+  public void convertFromGrpcStatus() {
+    // Without description
+    for (io.grpc.Status.Code grpcCanonicalCode : io.grpc.Status.Code.values()) {
+      io.grpc.Status grpcStatus = io.grpc.Status.fromCode(grpcCanonicalCode);
+      io.opencensus.trace.Status opencensusStatus = StatusConverter.fromGrpcStatus(grpcStatus);
+      assertThat(opencensusStatus.getCanonicalCode().toString())
+          .isEqualTo(grpcStatus.getCode().toString());
+      assertThat(opencensusStatus.getDescription()).isNull();
+    }
+
+    // With description
+    for (io.grpc.Status.Code grpcCanonicalCode : io.grpc.Status.Code.values()) {
+      io.grpc.Status grpcStatus =
+          io.grpc.Status.fromCode(grpcCanonicalCode).withDescription("This is my description");
+      io.opencensus.trace.Status opencensusStatus = StatusConverter.fromGrpcStatus(grpcStatus);
+      assertThat(opencensusStatus.getCanonicalCode().toString())
+          .isEqualTo(grpcStatus.getCode().toString());
+      assertThat(opencensusStatus.getDescription()).isEqualTo(grpcStatus.getDescription());
+    }
+  }
+
+  @Test
+  public void convertToGrpcCode() {
+    for (io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode :
+        io.opencensus.trace.Status.CanonicalCode.values()) {
+      io.grpc.Status.Code grpcCanonicalCode = StatusConverter.toGrpcCode(opencensusCanonicalCode);
+      assertThat(grpcCanonicalCode.toString()).isEqualTo(opencensusCanonicalCode.toString());
+    }
+  }
+
+  @Test
+  public void convertToGrpcStatus() {
+    // Without description
+    for (io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode :
+        io.opencensus.trace.Status.CanonicalCode.values()) {
+      io.opencensus.trace.Status opencensusStatus = opencensusCanonicalCode.toStatus();
+      io.grpc.Status grpcStatus = StatusConverter.toGrpcStatus(opencensusStatus);
+      assertThat(grpcStatus.getCode().toString())
+          .isEqualTo(opencensusStatus.getCanonicalCode().toString());
+      assertThat(grpcStatus.getDescription()).isNull();
+    }
+
+    // With description
+    for (io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode :
+        io.opencensus.trace.Status.CanonicalCode.values()) {
+      io.opencensus.trace.Status opencensusStatus =
+          opencensusCanonicalCode.toStatus().withDescription("This is my description");
+      io.grpc.Status grpcStatus = StatusConverter.toGrpcStatus(opencensusStatus);
+      assertThat(grpcStatus.getCode().toString())
+          .isEqualTo(opencensusStatus.getCanonicalCode().toString());
+      assertThat(grpcStatus.getDescription()).isEqualTo(opencensusStatus.getDescription());
+    }
+  }
+}
diff --git a/contrib/http_util/README.md b/contrib/http_util/README.md
new file mode 100644
index 0000000..9678fcb
--- /dev/null
+++ b/contrib/http_util/README.md
@@ -0,0 +1,41 @@
+# OpenCensus HTTP Util
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus HTTP Util for Java* is a collection of utilities for trace instrumentation when
+working with HTTP.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-http-util</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-contrib-http-util:0.16.1'
+```
+
+[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-grpc-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-grpc-util
+[grpc-url]: https://github.com/grpc/grpc-java
diff --git a/contrib/http_util/build.gradle b/contrib/http_util/build.gradle
new file mode 100644
index 0000000..a3c9f26
--- /dev/null
+++ b/contrib/http_util/build.gradle
@@ -0,0 +1,16 @@
+description = 'OpenCensus HTTP Util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.guava
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/CloudTraceFormat.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/CloudTraceFormat.java
new file mode 100644
index 0000000..77faa9f
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/CloudTraceFormat.java
@@ -0,0 +1,149 @@
+/*
+ * 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.http.util;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.primitives.UnsignedInts;
+import com.google.common.primitives.UnsignedLongs;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.NonNull;
+*/
+
+/**
+ * Implementation of the "X-Cloud-Trace-Context" format, defined by the Google Cloud Trace.
+ *
+ * <p>The supported format is the following:
+ *
+ * <pre>
+ * &lt;TRACE_ID&gt;/&lt;SPAN_ID&gt;[;o=&lt;TRACE_OPTIONS&gt;]
+ * </pre>
+ *
+ * <ul>
+ *   <li>TRACE_ID is a 32-character hex value;
+ *   <li>SPAN_ID is a decimal representation of a 64-bit unsigned long;
+ *   <li>TRACE_OPTIONS is a decimal representation of a 32-bit unsigned integer. The least
+ *       significant bit defines whether a request is traced (1 - enabled, 0 - disabled). Behaviors
+ *       of other bits are currently undefined. This value is optional. Although upstream service
+ *       may leave this value unset to leave the sampling decision up to downstream client, this
+ *       utility will always default it to 0 if absent.
+ * </ul>
+ *
+ * <p>Valid values:
+ *
+ * <ul>
+ *   <li>"105445aa7843bc8bf206b120001000/123;o=1"
+ *   <li>"105445aa7843bc8bf206b120001000/123"
+ *   <li>"105445aa7843bc8bf206b120001000/123;o=0"
+ * </ul>
+ */
+final class CloudTraceFormat extends TextFormat {
+  static final String HEADER_NAME = "X-Cloud-Trace-Context";
+  static final List<String> FIELDS = Collections.singletonList(HEADER_NAME);
+  static final char SPAN_ID_DELIMITER = '/';
+  static final String TRACE_OPTION_DELIMITER = ";o=";
+  static final String SAMPLED = "1";
+  static final String NOT_SAMPLED = "0";
+  static final TraceOptions OPTIONS_SAMPLED = TraceOptions.builder().setIsSampled(true).build();
+  static final TraceOptions OPTIONS_NOT_SAMPLED = TraceOptions.DEFAULT;
+  static final int TRACE_ID_SIZE = 2 * TraceId.SIZE;
+  static final int TRACE_OPTION_DELIMITER_SIZE = TRACE_OPTION_DELIMITER.length();
+  static final int SPAN_ID_START_POS = TRACE_ID_SIZE + 1;
+  // 32-digit TRACE_ID + 1 digit SPAN_ID_DELIMITER + at least 1 digit SPAN_ID
+  static final int MIN_HEADER_SIZE = SPAN_ID_START_POS + 1;
+  static final int CLOUD_TRACE_IS_SAMPLED = 0x1;
+  private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+
+  @Override
+  public List<String> fields() {
+    return FIELDS;
+  }
+
+  @Override
+  public <C /*>>> extends @NonNull Object*/> void inject(
+      SpanContext spanContext, C carrier, Setter<C> setter) {
+    checkNotNull(spanContext, "spanContext");
+    checkNotNull(setter, "setter");
+    checkNotNull(carrier, "carrier");
+    StringBuilder builder =
+        new StringBuilder()
+            .append(spanContext.getTraceId().toLowerBase16())
+            .append(SPAN_ID_DELIMITER)
+            .append(UnsignedLongs.toString(spanIdToLong(spanContext.getSpanId())))
+            .append(TRACE_OPTION_DELIMITER)
+            .append(spanContext.getTraceOptions().isSampled() ? SAMPLED : NOT_SAMPLED);
+
+    setter.put(carrier, HEADER_NAME, builder.toString());
+  }
+
+  @Override
+  public <C /*>>> extends @NonNull Object*/> SpanContext extract(C carrier, Getter<C> getter)
+      throws SpanContextParseException {
+    checkNotNull(carrier, "carrier");
+    checkNotNull(getter, "getter");
+    try {
+      String headerStr = getter.get(carrier, HEADER_NAME);
+      if (headerStr == null || headerStr.length() < MIN_HEADER_SIZE) {
+        throw new SpanContextParseException("Missing or too short header: " + HEADER_NAME);
+      }
+      checkArgument(headerStr.charAt(TRACE_ID_SIZE) == SPAN_ID_DELIMITER, "Invalid TRACE_ID size");
+
+      TraceId traceId = TraceId.fromLowerBase16(headerStr.subSequence(0, TRACE_ID_SIZE));
+      int traceOptionsPos = headerStr.indexOf(TRACE_OPTION_DELIMITER, TRACE_ID_SIZE);
+      CharSequence spanIdStr =
+          headerStr.subSequence(
+              SPAN_ID_START_POS, traceOptionsPos < 0 ? headerStr.length() : traceOptionsPos);
+      SpanId spanId = longToSpanId(UnsignedLongs.parseUnsignedLong(spanIdStr.toString(), 10));
+      TraceOptions traceOptions = OPTIONS_NOT_SAMPLED;
+      if (traceOptionsPos > 0) {
+        String traceOptionsStr = headerStr.substring(traceOptionsPos + TRACE_OPTION_DELIMITER_SIZE);
+        if ((UnsignedInts.parseUnsignedInt(traceOptionsStr, 10) & CLOUD_TRACE_IS_SAMPLED) != 0) {
+          traceOptions = OPTIONS_SAMPLED;
+        }
+      }
+      return SpanContext.create(traceId, spanId, traceOptions, TRACESTATE_DEFAULT);
+    } catch (IllegalArgumentException e) {
+      throw new SpanContextParseException("Invalid input", e);
+    }
+  }
+
+  // Using big-endian encoding.
+  private static SpanId longToSpanId(long x) {
+    ByteBuffer buffer = ByteBuffer.allocate(SpanId.SIZE);
+    buffer.putLong(x);
+    return SpanId.fromBytes(buffer.array());
+  }
+
+  // Using big-endian encoding.
+  private static long spanIdToLong(SpanId spanId) {
+    ByteBuffer buffer = ByteBuffer.allocate(SpanId.SIZE);
+    buffer.put(spanId.getBytes());
+    return buffer.getLong(0);
+  }
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpMeasureConstants.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpMeasureConstants.java
new file mode 100644
index 0000000..fd73b8a
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpMeasureConstants.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2018, 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.http.util;
+
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagKey;
+
+/**
+ * A helper class which holds OpenCensus's default HTTP {@link Measure}s and {@link TagKey}s.
+ *
+ * <p>{@link Measure}s and {@link TagKey}s in this class are all public for other
+ * libraries/frameworks to reference and use.
+ *
+ * @since 0.13
+ */
+public final class HttpMeasureConstants {
+
+  private HttpMeasureConstants() {}
+
+  private static final String UNIT_COUNT = "1";
+  private static final String UNIT_SIZE_BYTE = "By";
+  private static final String UNIT_LATENCY_MS = "ms";
+
+  /**
+   * {@link Measure} for the client-side total bytes sent in request body (not including headers).
+   * This is uncompressed bytes.
+   *
+   * @since 0.13
+   */
+  public static final MeasureLong HTTP_CLIENT_SENT_BYTES =
+      Measure.MeasureLong.create(
+          "opencensus.io/http/client/sent_bytes",
+          "Client-side total bytes sent in request body (uncompressed)",
+          UNIT_SIZE_BYTE);
+
+  /**
+   * {@link Measure} for the client-side total bytes received in response bodies (not including
+   * headers but including error responses with bodies). Should be measured from actual bytes
+   * received and read, not the value of the Content-Length header. This is uncompressed bytes.
+   * Responses with no body should record 0 for this value.
+   *
+   * @since 0.13
+   */
+  public static final MeasureLong HTTP_CLIENT_RECEIVED_BYTES =
+      Measure.MeasureLong.create(
+          "opencensus.io/http/client/received_bytes",
+          "Client-side total bytes received in response bodies (uncompressed)",
+          UNIT_SIZE_BYTE);
+
+  /**
+   * {@link Measure} for the client-side time between first byte of request headers sent to last
+   * byte of response received, or terminal error.
+   *
+   * @since 0.13
+   */
+  public static final MeasureDouble HTTP_CLIENT_ROUNDTRIP_LATENCY =
+      Measure.MeasureDouble.create(
+          "opencensus.io/http/client/roundtrip_latency",
+          "Client-side time between first byte of request headers sent to last byte of response "
+              + "received, or terminal error",
+          UNIT_LATENCY_MS);
+
+  /**
+   * {@link Measure} for the server-side total bytes received in request body (not including
+   * headers). This is uncompressed bytes.
+   *
+   * @since 0.13
+   */
+  public static final MeasureLong HTTP_SERVER_RECEIVED_BYTES =
+      Measure.MeasureLong.create(
+          "opencensus.io/http/server/received_bytes",
+          "Server-side total bytes received in request body (uncompressed)",
+          UNIT_SIZE_BYTE);
+
+  /**
+   * {@link Measure} for the server-side total bytes sent in response bodies (not including headers
+   * but including error responses with bodies). Should be measured from actual bytes written and
+   * sent, not the value of the Content-Length header. This is uncompressed bytes. Responses with no
+   * body should record 0 for this value.
+   *
+   * @since 0.13
+   */
+  public static final MeasureLong HTTP_SERVER_SENT_BYTES =
+      Measure.MeasureLong.create(
+          "opencensus.io/http/server/sent_bytes",
+          "Server-side total bytes sent in response bodies (uncompressed)",
+          UNIT_SIZE_BYTE);
+
+  /**
+   * {@link Measure} for the server-side time between first byte of request headers received to last
+   * byte of response sent, or terminal error.
+   *
+   * @since 0.13
+   */
+  public static final MeasureDouble HTTP_SERVER_LATENCY =
+      Measure.MeasureDouble.create(
+          "opencensus.io/http/server/server_latency",
+          "Server-side time between first byte of request headers received to last byte of "
+              + "response sent, or terminal error",
+          UNIT_LATENCY_MS);
+
+  /**
+   * {@link TagKey} for the value of the client-side HTTP host header.
+   *
+   * @since 0.13
+   */
+  public static final TagKey HTTP_CLIENT_HOST = TagKey.create("http_client_host");
+
+  /**
+   * {@link TagKey} for the value of the server-side HTTP host header.
+   *
+   * @since 0.13
+   */
+  public static final TagKey HTTP_SERVER_HOST = TagKey.create("http_server_host");
+
+  /**
+   * {@link TagKey} for the numeric client-side HTTP response status code (e.g. 200, 404, 500). If a
+   * transport error occurred and no status code was read, use "error" as the {@code TagValue}.
+   *
+   * @since 0.13
+   */
+  public static final TagKey HTTP_CLIENT_STATUS = TagKey.create("http_client_status");
+
+  /**
+   * {@link TagKey} for the numeric server-side HTTP response status code (e.g. 200, 404, 500). If a
+   * transport error occurred and no status code was written, use "error" as the {@code TagValue}.
+   *
+   * @since 0.13
+   */
+  public static final TagKey HTTP_SERVER_STATUS = TagKey.create("http_server_status");
+
+  /**
+   * {@link TagKey} for the client-side URL path (not including query string) in the request.
+   *
+   * @since 0.13
+   */
+  public static final TagKey HTTP_CLIENT_PATH = TagKey.create("http_client_path");
+
+  /**
+   * {@link TagKey} for the server-side URL path (not including query string) in the request.
+   *
+   * @since 0.13
+   */
+  public static final TagKey HTTP_SERVER_PATH = TagKey.create("http_server_path");
+
+  /**
+   * {@link TagKey} for the client-side HTTP method of the request, capitalized (GET, POST, etc.).
+   *
+   * @since 0.13
+   */
+  public static final TagKey HTTP_CLIENT_METHOD = TagKey.create("http_client_method");
+
+  /**
+   * {@link TagKey} for the server-side HTTP method of the request, capitalized (GET, POST, etc.).
+   *
+   * @since 0.13
+   */
+  public static final TagKey HTTP_SERVER_METHOD = TagKey.create("http_server_method");
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpPropagationUtil.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpPropagationUtil.java
new file mode 100644
index 0000000..779be8d
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpPropagationUtil.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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.http.util;
+
+import io.opencensus.trace.propagation.TextFormat;
+
+/**
+ * Utility class to get all supported {@link TextFormat}.
+ *
+ * @since 0.11.0
+ */
+public class HttpPropagationUtil {
+
+  private HttpPropagationUtil() {}
+
+  /**
+   * Returns the Stack Driver format implementation. The header specification for this format is
+   * "X-Cloud-Trace-Context: &lt;TRACE_ID&gt;/&lt;SPAN_ID&gt;[;o=&lt;TRACE_TRUE&gt;]". See this <a
+   * href="https://cloud.google.com/trace/docs/support">page</a> for more information.
+   *
+   * @since 0.11.0
+   * @return the Stack Driver format.
+   */
+  public static TextFormat getCloudTraceFormat() {
+    return new CloudTraceFormat();
+  }
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViewConstants.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViewConstants.java
new file mode 100644
index 0000000..54ad20c
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViewConstants.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2018, 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.http.util;
+
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_METHOD;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_PATH;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_RECEIVED_BYTES;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_SENT_BYTES;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_STATUS;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_LATENCY;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_METHOD;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_PATH;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_RECEIVED_BYTES;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_SENT_BYTES;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_STATUS;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.View;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * A helper class that holds OpenCensus's default HTTP {@link View}s.
+ *
+ * <p>{@link View}s in this class are all public for other libraries/frameworks to reference and
+ * use.
+ *
+ * @since 0.13
+ */
+public final class HttpViewConstants {
+
+  private HttpViewConstants() {}
+
+  @VisibleForTesting static final Aggregation COUNT = Count.create();
+
+  @VisibleForTesting
+  static final Aggregation SIZE_DISTRIBUTION =
+      Distribution.create(
+          BucketBoundaries.create(
+              Collections.<Double>unmodifiableList(
+                  Arrays.<Double>asList(
+                      0.0,
+                      1024.0,
+                      2048.0,
+                      4096.0,
+                      16384.0,
+                      65536.0,
+                      262144.0,
+                      1048576.0,
+                      4194304.0,
+                      16777216.0,
+                      67108864.0,
+                      268435456.0,
+                      1073741824.0,
+                      4294967296.0))));
+
+  @VisibleForTesting
+  static final Aggregation LATENCY_DISTRIBUTION =
+      Distribution.create(
+          BucketBoundaries.create(
+              Collections.<Double>unmodifiableList(
+                  Arrays.<Double>asList(
+                      0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0, 25.0, 30.0,
+                      40.0, 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0, 300.0, 400.0,
+                      500.0, 650.0, 800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0,
+                      100000.0))));
+
+  /**
+   * {@link View} for count of client-side HTTP requests completed.
+   *
+   * @since 0.13
+   */
+  public static final View HTTP_CLIENT_COMPLETED_COUNT_VIEW =
+      View.create(
+          View.Name.create("opencensus.io/http/client/completed_count"),
+          "Count of client-side HTTP requests completed",
+          HTTP_CLIENT_ROUNDTRIP_LATENCY,
+          COUNT,
+          Arrays.asList(HTTP_CLIENT_METHOD, HTTP_CLIENT_PATH));
+
+  /**
+   * {@link View} for size distribution of client-side HTTP request body.
+   *
+   * @since 0.13
+   */
+  public static final View HTTP_CLIENT_SENT_BYTES_VIEW =
+      View.create(
+          View.Name.create("opencensus.io/http/client/sent_bytes"),
+          "Size distribution of client-side HTTP request body",
+          HTTP_CLIENT_SENT_BYTES,
+          SIZE_DISTRIBUTION,
+          Arrays.asList(HTTP_CLIENT_METHOD, HTTP_CLIENT_PATH));
+
+  /**
+   * {@link View} for size distribution of client-side HTTP response body.
+   *
+   * @since 0.13
+   */
+  public static final View HTTP_CLIENT_RECEIVED_BYTES_VIEW =
+      View.create(
+          View.Name.create("opencensus.io/http/client/received_bytes"),
+          "Size distribution of client-side HTTP response body",
+          HTTP_CLIENT_RECEIVED_BYTES,
+          SIZE_DISTRIBUTION,
+          Arrays.asList(HTTP_CLIENT_METHOD, HTTP_CLIENT_PATH));
+
+  /**
+   * {@link View} for roundtrip latency distribution of client-side HTTP requests.
+   *
+   * @since 0.13
+   */
+  public static final View HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW =
+      View.create(
+          View.Name.create("opencensus.io/http/client/roundtrip_latency"),
+          "Roundtrip latency distribution of client-side HTTP requests",
+          HTTP_CLIENT_ROUNDTRIP_LATENCY,
+          LATENCY_DISTRIBUTION,
+          Arrays.asList(HTTP_CLIENT_METHOD, HTTP_CLIENT_PATH, HTTP_CLIENT_STATUS));
+
+  /**
+   * {@link View} for count of server-side HTTP requests serving completed.
+   *
+   * @since 0.13
+   */
+  public static final View HTTP_SERVER_COMPLETED_COUNT_VIEW =
+      View.create(
+          View.Name.create("opencensus.io/http/server/completed_count"),
+          "Count of HTTP server-side requests serving completed",
+          HTTP_SERVER_LATENCY,
+          COUNT,
+          Arrays.asList(HTTP_SERVER_METHOD, HTTP_SERVER_PATH));
+
+  /**
+   * {@link View} for size distribution of server-side HTTP request body.
+   *
+   * @since 0.13
+   */
+  public static final View HTTP_SERVER_RECEIVED_BYTES_VIEW =
+      View.create(
+          View.Name.create("opencensus.io/http/server/received_bytes"),
+          "Size distribution of server-side HTTP request body",
+          HTTP_SERVER_RECEIVED_BYTES,
+          SIZE_DISTRIBUTION,
+          Arrays.asList(HTTP_SERVER_METHOD, HTTP_SERVER_PATH));
+
+  /**
+   * {@link View} for size distribution of server-side HTTP response body.
+   *
+   * @since 0.13
+   */
+  public static final View HTTP_SERVER_SENT_BYTES_VIEW =
+      View.create(
+          View.Name.create("opencensus.io/http/server/sent_bytes"),
+          "Size distribution of server-side HTTP response body",
+          HTTP_SERVER_SENT_BYTES,
+          SIZE_DISTRIBUTION,
+          Arrays.asList(HTTP_SERVER_METHOD, HTTP_SERVER_PATH));
+
+  /**
+   * {@link View} for latency distribution of server-side HTTP requests serving.
+   *
+   * @since 0.13
+   */
+  public static final View HTTP_SERVER_LATENCY_VIEW =
+      View.create(
+          View.Name.create("opencensus.io/http/server/server_latency"),
+          "Latency distribution of server-side HTTP requests serving",
+          HTTP_SERVER_LATENCY,
+          LATENCY_DISTRIBUTION,
+          Arrays.asList(HTTP_SERVER_METHOD, HTTP_SERVER_PATH, HTTP_SERVER_STATUS));
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViews.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViews.java
new file mode 100644
index 0000000..9e3b984
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViews.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018, 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.http.util;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewManager;
+
+/**
+ * A helper class that allows users to register HTTP views easily.
+ *
+ * @since 0.13
+ */
+public final class HttpViews {
+
+  private HttpViews() {}
+
+  @VisibleForTesting
+  static final ImmutableSet<View> HTTP_SERVER_VIEWS_SET =
+      ImmutableSet.of(
+          HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW,
+          HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW,
+          HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW,
+          HttpViewConstants.HTTP_SERVER_LATENCY_VIEW);
+
+  @VisibleForTesting
+  static final ImmutableSet<View> HTTP_CLIENT_VIEWS_SET =
+      ImmutableSet.of(
+          HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW,
+          HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW,
+          HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW,
+          HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW);
+
+  /**
+   * Register all default client views.
+   *
+   * <p>It is recommended to call this method before doing any HTTP call to avoid missing stats.
+   *
+   * @since 0.13
+   */
+  public static final void registerAllClientViews() {
+    registerAllClientViews(Stats.getViewManager());
+  }
+
+  @VisibleForTesting
+  static void registerAllClientViews(ViewManager viewManager) {
+    for (View view : HTTP_CLIENT_VIEWS_SET) {
+      viewManager.registerView(view);
+    }
+  }
+
+  /**
+   * Register all default server views.
+   *
+   * <p>It is recommended to call this method before doing any HTTP call to avoid missing stats.
+   *
+   * @since 0.13
+   */
+  public static final void registerAllServerViews() {
+    registerAllServerViews(Stats.getViewManager());
+  }
+
+  @VisibleForTesting
+  static void registerAllServerViews(ViewManager viewManager) {
+    for (View view : HTTP_SERVER_VIEWS_SET) {
+      viewManager.registerView(view);
+    }
+  }
+
+  /**
+   * Register all default views. Equivalent with calling {@link #registerAllClientViews()} and
+   * {@link #registerAllServerViews()}.
+   *
+   * <p>It is recommended to call this method before doing any HTTP call to avoid missing stats.
+   *
+   * @since 0.13
+   */
+  public static final void registerAllViews() {
+    registerAllViews(Stats.getViewManager());
+  }
+
+  @VisibleForTesting
+  static void registerAllViews(ViewManager viewManager) {
+    registerAllClientViews(viewManager);
+    registerAllServerViews(viewManager);
+  }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/CloudTraceFormatTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/CloudTraceFormatTest.java
new file mode 100644
index 0000000..4492a40
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/CloudTraceFormatTest.java
@@ -0,0 +1,295 @@
+/*
+ * 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.http.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.HEADER_NAME;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.NOT_SAMPLED;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.SAMPLED;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.SPAN_ID_DELIMITER;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.TRACE_OPTION_DELIMITER;
+
+import com.google.common.primitives.UnsignedLong;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link CloudTraceFormat}. */
+@RunWith(JUnit4.class)
+public final class CloudTraceFormatTest {
+  private final CloudTraceFormat cloudTraceFormat = new CloudTraceFormat();
+
+  private static final String TRACE_ID_BASE16 = "ff000000000000000000000000000041";
+  private static final String TRACE_ID_BASE16_SHORT = "ff00000000000041";
+  private static final String TRACE_ID_BASE16_LONG = "0000" + TRACE_ID_BASE16;
+  private static final String TRACE_ID_BASE16_INVALID = "ff00000000000000000000000abcdefg";
+  private static final String SPAN_ID_BASE16 = "ff00000000000041";
+  private static final String SPAN_ID_BASE10 = UnsignedLong.valueOf(SPAN_ID_BASE16, 16).toString();
+  private static final String SPAN_ID_BASE10_NEGATIVE = "-12345";
+  private static final String SPAN_ID_BASE10_MAX_UNSIGNED_LONG = UnsignedLong.MAX_VALUE.toString();
+  private static final String SPAN_ID_BASE16_MAX_UNSIGNED_LONG =
+      UnsignedLong.MAX_VALUE.toString(16);
+  private static final String SPAN_ID_BASE10_VERY_LONG =
+      SPAN_ID_BASE10_MAX_UNSIGNED_LONG + SPAN_ID_BASE10_MAX_UNSIGNED_LONG;
+  private static final String SPAN_ID_BASE10_INVALID = "0x12345";
+  private static final String OPTIONS_SAMPLED_MORE_BITS = "11"; // 1011
+  private static final String OPTIONS_NOT_SAMPLED_MORE_BITS = "10"; // 1010
+  private static final String OPTIONS_NEGATIVE = "-1";
+  private static final String OPTIONS_INVALID = "0x1";
+
+  private static final TraceId TRACE_ID = TraceId.fromLowerBase16(TRACE_ID_BASE16);
+  private static final SpanId SPAN_ID = SpanId.fromLowerBase16(SPAN_ID_BASE16);
+  private static final SpanId SPAN_ID_MAX =
+      SpanId.fromLowerBase16(SPAN_ID_BASE16_MAX_UNSIGNED_LONG);
+
+  private static final TraceOptions TRACE_OPTIONS_SAMPLED =
+      TraceOptions.builder().setIsSampled(true).build();
+  private static final TraceOptions TRACE_OPTIONS_NOT_SAMPLED = TraceOptions.DEFAULT;
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+  private final Setter<Map<String, String>> setter =
+      new Setter<Map<String, String>>() {
+        @Override
+        public void put(Map<String, String> carrier, String key, String value) {
+          carrier.put(key, value);
+        }
+      };
+  private final Getter<Map<String, String>> getter =
+      new Getter<Map<String, String>>() {
+        @Nullable
+        @Override
+        public String get(Map<String, String> carrier, String key) {
+          return carrier.get(key);
+        }
+      };
+
+  private static String constructHeader(String traceId, String spanId) {
+    return traceId + SPAN_ID_DELIMITER + spanId;
+  }
+
+  private static String constructHeader(String traceId, String spanId, String traceOptions) {
+    return traceId + SPAN_ID_DELIMITER + spanId + TRACE_OPTION_DELIMITER + traceOptions;
+  }
+
+  private void parseSuccess(String headerValue, SpanContext expected)
+      throws SpanContextParseException {
+    Map<String, String> header = new HashMap<String, String>();
+    header.put(HEADER_NAME, headerValue);
+    assertThat(cloudTraceFormat.extract(header, getter)).isEqualTo(expected);
+  }
+
+  private void parseFailure(
+      String headerValue, Class<? extends Throwable> expectedThrown, String expectedMessage)
+      throws SpanContextParseException {
+    Map<String, String> header = new HashMap<String, String>();
+    header.put(HEADER_NAME, headerValue);
+    thrown.expect(expectedThrown);
+    thrown.expectMessage(expectedMessage);
+    cloudTraceFormat.extract(header, getter);
+  }
+
+  @Test
+  public void serializeSampledContextShouldSucceed() throws SpanContextParseException {
+    Map<String, String> carrier = new HashMap<String, String>();
+    cloudTraceFormat.inject(
+        SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_SAMPLED), carrier, setter);
+    assertThat(carrier)
+        .containsExactly(HEADER_NAME, constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, SAMPLED));
+  }
+
+  @Test
+  public void serializeNotSampledContextShouldSucceed() throws SpanContextParseException {
+    Map<String, String> carrier = new HashMap<String, String>();
+    cloudTraceFormat.inject(
+        SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_NOT_SAMPLED), carrier, setter);
+    assertThat(carrier)
+        .containsExactly(
+            HEADER_NAME, constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, NOT_SAMPLED));
+  }
+
+  @Test
+  public void parseSampledShouldSucceed() throws SpanContextParseException {
+    parseSuccess(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, OPTIONS_SAMPLED_MORE_BITS),
+        SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_SAMPLED));
+  }
+
+  @Test
+  public void parseNotSampledShouldSucceed() throws SpanContextParseException {
+    parseSuccess(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, OPTIONS_NOT_SAMPLED_MORE_BITS),
+        SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_NOT_SAMPLED));
+  }
+
+  @Test
+  public void parseMissingTraceOptionsShouldSucceed() throws SpanContextParseException {
+    parseSuccess(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10),
+        SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_NOT_SAMPLED));
+  }
+
+  @Test
+  public void parseEmptyTraceOptionsShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, ""),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseNegativeTraceOptionsShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, OPTIONS_NEGATIVE),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseInvalidTraceOptionsShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, OPTIONS_INVALID),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseMissingHeaderShouldFail() throws SpanContextParseException {
+    Map<String, String> headerMissing = new HashMap<String, String>();
+    thrown.expect(SpanContextParseException.class);
+    thrown.expectMessage("Missing or too short header: X-Cloud-Trace-Context");
+    cloudTraceFormat.extract(headerMissing, getter);
+  }
+
+  @Test
+  public void parseEmptyHeaderShouldFail() throws SpanContextParseException {
+    parseFailure(
+        "", SpanContextParseException.class, "Missing or too short header: X-Cloud-Trace-Context");
+  }
+
+  @Test
+  public void parseShortHeaderShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16, ""),
+        SpanContextParseException.class,
+        "Missing or too short header: X-Cloud-Trace-Context");
+  }
+
+  @Test
+  public void parseShortTraceIdShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16_SHORT, SPAN_ID_BASE10, SAMPLED),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseLongTraceIdShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16_LONG, SPAN_ID_BASE10, SAMPLED),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseMissingTraceIdShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader("", SPAN_ID_BASE10_VERY_LONG, SAMPLED),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseInvalidTraceIdShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16_INVALID, SPAN_ID_BASE10, SAMPLED),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseMissingSpanIdDelimiterShouldFail() throws SpanContextParseException {
+    parseFailure(TRACE_ID_BASE16_LONG, SpanContextParseException.class, "Invalid input");
+  }
+
+  @Test
+  public void parseNegativeSpanIdShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10_NEGATIVE, SAMPLED),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseMaxUnsignedLongSpanIdShouldSucceed() throws SpanContextParseException {
+    parseSuccess(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10_MAX_UNSIGNED_LONG, SAMPLED),
+        SpanContext.create(TRACE_ID, SPAN_ID_MAX, TRACE_OPTIONS_SAMPLED));
+  }
+
+  @Test
+  public void parseOverflowSpanIdShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10_VERY_LONG, SAMPLED),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseInvalidSpanIdShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10_INVALID, SAMPLED),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void parseMissingSpanIdShouldFail() throws SpanContextParseException {
+    parseFailure(
+        constructHeader(TRACE_ID_BASE16, "", SAMPLED),
+        SpanContextParseException.class,
+        "Invalid input");
+  }
+
+  @Test
+  public void fieldsShouldMatch() {
+    assertThat(cloudTraceFormat.fields()).containsExactly(HEADER_NAME);
+  }
+
+  @Test
+  public void parseWithShortSpanIdAndSamplingShouldSucceed() throws SpanContextParseException {
+    final String spanId = "1";
+    ByteBuffer buffer = ByteBuffer.allocate(SpanId.SIZE);
+    buffer.putLong(Long.parseLong(spanId));
+    SpanId expectedSpanId = SpanId.fromBytes(buffer.array());
+    parseSuccess(
+        constructHeader(TRACE_ID_BASE16, spanId, SAMPLED),
+        SpanContext.create(TRACE_ID, expectedSpanId, TRACE_OPTIONS_SAMPLED));
+  }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpMeasureConstantsTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpMeasureConstantsTest.java
new file mode 100644
index 0000000..dd1c20f
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpMeasureConstantsTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018, 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.http.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.tags.TagKey;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link HttpMeasureConstants}. */
+@RunWith(JUnit4.class)
+public class HttpMeasureConstantsTest {
+
+  @Test
+  public void constants() {
+    // Test TagKeys
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_STATUS)
+        .isEqualTo(TagKey.create("http_client_status"));
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_METHOD)
+        .isEqualTo(TagKey.create("http_client_method"));
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_PATH).isEqualTo(TagKey.create("http_client_path"));
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_HOST).isEqualTo(TagKey.create("http_client_host"));
+    assertThat(HttpMeasureConstants.HTTP_SERVER_STATUS)
+        .isEqualTo(TagKey.create("http_server_status"));
+    assertThat(HttpMeasureConstants.HTTP_SERVER_METHOD)
+        .isEqualTo(TagKey.create("http_server_method"));
+    assertThat(HttpMeasureConstants.HTTP_SERVER_PATH).isEqualTo(TagKey.create("http_server_path"));
+    assertThat(HttpMeasureConstants.HTTP_SERVER_HOST).isEqualTo(TagKey.create("http_server_host"));
+
+    // Test measures
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_SENT_BYTES.getUnit()).isEqualTo("By");
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_RECEIVED_BYTES.getUnit()).isEqualTo("By");
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY.getUnit()).isEqualTo("ms");
+    assertThat(HttpMeasureConstants.HTTP_SERVER_RECEIVED_BYTES.getUnit()).isEqualTo("By");
+    assertThat(HttpMeasureConstants.HTTP_SERVER_SENT_BYTES.getUnit()).isEqualTo("By");
+    assertThat(HttpMeasureConstants.HTTP_SERVER_LATENCY.getUnit()).isEqualTo("ms");
+
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_SENT_BYTES.getName())
+        .contains("opencensus.io/http/client");
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_RECEIVED_BYTES.getName())
+        .contains("opencensus.io/http/client");
+    assertThat(HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY.getName())
+        .contains("opencensus.io/http/client");
+    assertThat(HttpMeasureConstants.HTTP_SERVER_RECEIVED_BYTES.getName())
+        .contains("opencensus.io/http/server");
+    assertThat(HttpMeasureConstants.HTTP_SERVER_SENT_BYTES.getName())
+        .contains("opencensus.io/http/server");
+    assertThat(HttpMeasureConstants.HTTP_SERVER_LATENCY.getName())
+        .contains("opencensus.io/http/server");
+  }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpPropagationUtilTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpPropagationUtilTest.java
new file mode 100644
index 0000000..f84a4da
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpPropagationUtilTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018, 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.http.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.propagation.TextFormat;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link HttpPropagationUtil}. */
+@RunWith(JUnit4.class)
+public class HttpPropagationUtilTest {
+
+  @Test
+  public void cloudTraceFormatNotNull() {
+    TextFormat cloudTraceFormat = HttpPropagationUtil.getCloudTraceFormat();
+    assertThat(cloudTraceFormat).isNotNull();
+    assertThat(cloudTraceFormat).isInstanceOf(CloudTraceFormat.class);
+  }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewConstantsTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewConstantsTest.java
new file mode 100644
index 0000000..d008348
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewConstantsTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2018, 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.http.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link HttpViewConstants}. */
+@RunWith(JUnit4.class)
+public class HttpViewConstantsTest {
+
+  @Test
+  public void constants() {
+    // Test aggregations, and their bucket boundaries (if they are Distribution).
+    assertThat(HttpViewConstants.COUNT).isEqualTo(Count.create());
+    assertThat(HttpViewConstants.SIZE_DISTRIBUTION).isInstanceOf(Distribution.class);
+    assertThat(
+            ((Distribution) HttpViewConstants.SIZE_DISTRIBUTION)
+                .getBucketBoundaries()
+                .getBoundaries())
+        .containsExactly(
+            0.0,
+            1024.0,
+            2048.0,
+            4096.0,
+            16384.0,
+            65536.0,
+            262144.0,
+            1048576.0,
+            4194304.0,
+            16777216.0,
+            67108864.0,
+            268435456.0,
+            1073741824.0,
+            4294967296.0)
+        .inOrder();
+    assertThat(HttpViewConstants.LATENCY_DISTRIBUTION).isInstanceOf(Distribution.class);
+    assertThat(
+            ((Distribution) HttpViewConstants.LATENCY_DISTRIBUTION)
+                .getBucketBoundaries()
+                .getBoundaries())
+        .containsExactly(
+            0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0, 25.0, 30.0, 40.0, 50.0,
+            65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0, 300.0, 400.0, 500.0, 650.0, 800.0,
+            1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0, 100000.0)
+        .inOrder();
+
+    // Test views.
+    assertThat(HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW.getName().asString())
+        .contains("opencensus.io/http/client");
+    assertThat(HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW.getName().asString())
+        .contains("opencensus.io/http/client");
+    assertThat(HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW.getName().asString())
+        .contains("opencensus.io/http/client");
+    assertThat(HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW.getName().asString())
+        .contains("opencensus.io/http/client");
+    assertThat(HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW.getName().asString())
+        .contains("opencensus.io/http/server");
+    assertThat(HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW.getName().asString())
+        .contains("opencensus.io/http/server");
+    assertThat(HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW.getName().asString())
+        .contains("opencensus.io/http/server");
+    assertThat(HttpViewConstants.HTTP_SERVER_LATENCY_VIEW.getName().asString())
+        .contains("opencensus.io/http/server");
+
+    assertThat(HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW.getMeasure())
+        .isEqualTo(HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY);
+    assertThat(HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW.getMeasure())
+        .isEqualTo(HttpMeasureConstants.HTTP_CLIENT_SENT_BYTES);
+    assertThat(HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW.getMeasure())
+        .isEqualTo(HttpMeasureConstants.HTTP_CLIENT_RECEIVED_BYTES);
+    assertThat(HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW.getMeasure())
+        .isEqualTo(HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY);
+    assertThat(HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW.getMeasure())
+        .isEqualTo(HttpMeasureConstants.HTTP_SERVER_LATENCY);
+    assertThat(HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW.getMeasure())
+        .isEqualTo(HttpMeasureConstants.HTTP_SERVER_RECEIVED_BYTES);
+    assertThat(HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW.getMeasure())
+        .isEqualTo(HttpMeasureConstants.HTTP_SERVER_SENT_BYTES);
+    assertThat(HttpViewConstants.HTTP_SERVER_LATENCY_VIEW.getMeasure())
+        .isEqualTo(HttpMeasureConstants.HTTP_SERVER_LATENCY);
+
+    assertThat(HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW.getAggregation())
+        .isEqualTo(HttpViewConstants.COUNT);
+    assertThat(HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW.getAggregation())
+        .isEqualTo(HttpViewConstants.SIZE_DISTRIBUTION);
+    assertThat(HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW.getAggregation())
+        .isEqualTo(HttpViewConstants.SIZE_DISTRIBUTION);
+    assertThat(HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW.getAggregation())
+        .isEqualTo(HttpViewConstants.LATENCY_DISTRIBUTION);
+    assertThat(HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW.getAggregation())
+        .isEqualTo(HttpViewConstants.COUNT);
+    assertThat(HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW.getAggregation())
+        .isEqualTo(HttpViewConstants.SIZE_DISTRIBUTION);
+    assertThat(HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW.getAggregation())
+        .isEqualTo(HttpViewConstants.SIZE_DISTRIBUTION);
+    assertThat(HttpViewConstants.HTTP_SERVER_LATENCY_VIEW.getAggregation())
+        .isEqualTo(HttpViewConstants.LATENCY_DISTRIBUTION);
+
+    assertThat(HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW.getColumns())
+        .containsExactly(
+            HttpMeasureConstants.HTTP_CLIENT_METHOD, HttpMeasureConstants.HTTP_CLIENT_PATH);
+    assertThat(HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW.getColumns())
+        .containsExactly(
+            HttpMeasureConstants.HTTP_CLIENT_METHOD, HttpMeasureConstants.HTTP_CLIENT_PATH);
+    assertThat(HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW.getColumns())
+        .containsExactly(
+            HttpMeasureConstants.HTTP_CLIENT_METHOD, HttpMeasureConstants.HTTP_CLIENT_PATH);
+    assertThat(HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW.getColumns())
+        .containsExactly(
+            HttpMeasureConstants.HTTP_CLIENT_METHOD,
+            HttpMeasureConstants.HTTP_CLIENT_PATH,
+            HttpMeasureConstants.HTTP_CLIENT_STATUS);
+    assertThat(HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW.getColumns())
+        .containsExactly(
+            HttpMeasureConstants.HTTP_SERVER_METHOD, HttpMeasureConstants.HTTP_SERVER_PATH);
+    assertThat(HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW.getColumns())
+        .containsExactly(
+            HttpMeasureConstants.HTTP_SERVER_METHOD, HttpMeasureConstants.HTTP_SERVER_PATH);
+    assertThat(HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW.getColumns())
+        .containsExactly(
+            HttpMeasureConstants.HTTP_SERVER_METHOD, HttpMeasureConstants.HTTP_SERVER_PATH);
+    assertThat(HttpViewConstants.HTTP_SERVER_LATENCY_VIEW.getColumns())
+        .containsExactly(
+            HttpMeasureConstants.HTTP_SERVER_METHOD,
+            HttpMeasureConstants.HTTP_SERVER_PATH,
+            HttpMeasureConstants.HTTP_SERVER_STATUS);
+  }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewsTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewsTest.java
new file mode 100644
index 0000000..8adf0a5
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewsTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018, 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.http.util;
+
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewManager;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Test for {@link HttpViews}. */
+@RunWith(JUnit4.class)
+public class HttpViewsTest {
+
+  @Mock ViewManager viewManager;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void registerClientViews() {
+    HttpViews.registerAllClientViews(viewManager);
+    for (View view : HttpViews.HTTP_CLIENT_VIEWS_SET) {
+      verify(viewManager).registerView(view);
+    }
+  }
+
+  @Test
+  public void registerServerViews() {
+    HttpViews.registerAllServerViews(viewManager);
+    for (View view : HttpViews.HTTP_SERVER_VIEWS_SET) {
+      verify(viewManager).registerView(view);
+    }
+  }
+
+  @Test
+  public void registerAll() {
+    HttpViews.registerAllViews(viewManager);
+    for (View view : HttpViews.HTTP_CLIENT_VIEWS_SET) {
+      verify(viewManager).registerView(view);
+    }
+    for (View view : HttpViews.HTTP_SERVER_VIEWS_SET) {
+      verify(viewManager).registerView(view);
+    }
+  }
+}
diff --git a/contrib/log_correlation/log4j2/README.md b/contrib/log_correlation/log4j2/README.md
new file mode 100644
index 0000000..a5bf144
--- /dev/null
+++ b/contrib/log_correlation/log4j2/README.md
@@ -0,0 +1,88 @@
+# OpenCensus Log4j 2 Log Correlation
+
+This subproject is currently experimental, so it may be redesigned or removed in the future.  It
+will remain experimental until we have a specification for a log correlation feature in
+[opencensus-specs](https://github.com/census-instrumentation/opencensus-specs/)
+(issue [#123](https://github.com/census-instrumentation/opencensus-specs/issues/123)).
+
+The `opencensus-contrib-log-correlation-log4j2` artifact provides a
+[Log4j 2](https://logging.apache.org/log4j/2.x/)
+[`ContextDataInjector`](https://logging.apache.org/log4j/2.x/manual/extending.html#Custom_ContextDataInjector)
+that automatically adds tracing data to the context of Log4j
+[`LogEvent`](https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html)s.
+The class name is
+`OpenCensusTraceContextDataInjector`. `OpenCensusTraceContextDataInjector` adds the current trace
+ID, span ID, and sampling decision to each `LogEvent`, so that they can be accessed with
+[`LogEvent.getContextData()`](https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getContextData())
+or included in a layout.
+
+See
+https://github.com/census-ecosystem/opencensus-experiments/tree/master/java/log_correlation/log4j2
+for a demo that uses this library to correlate logs and traces in Stackdriver.
+
+## Instructions
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-log-correlation-log4j2</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+runtime 'io.opencensus:opencensus-contrib-log-correlation-log4j2:0.16.1'
+```
+
+### Configure the `OpenCensusTraceContextDataInjector`
+
+#### Specify the `ContextDataInjector` override
+
+Override Log4j's default `ContextDataInjector` by setting the system property
+`log4j2.contextDataInjector` to the full name of the class,
+`io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector`.
+
+#### Choose when to add tracing data to log events
+
+The following system property controls the decision to add tracing data from the current span to a
+log event:
+
+`io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.spanSelection`
+
+The allowed values are:
+
+* `ALL_SPANS`: adds tracing data to all log events (default)
+
+* `NO_SPANS`: disables the log correlation feature
+
+* `SAMPLED_SPANS`: adds tracing data to log events when the current span is sampled
+
+### Add the tracing data to log entries
+
+`opencensus-contrib-log-correlation-log4j2` adds the following key-value pairs to the `LogEvent`
+context:
+
+* `opencensusTraceId` - the lowercase base16 encoding of the current trace ID
+* `opencensusSpanId` - the lowercase base16 encoding of the current span ID
+* `opencensusTraceSampled` - the sampling decision of the current span ("true" or "false")
+
+These values can be accessed from layouts with
+[Context Map Lookup](http://logging.apache.org/log4j/2.x/manual/lookups.html#ContextMapLookup).  For
+example, the trace ID can be accessed with `$${ctx:opencensusTraceId}`.  The values can also be
+accessed with the `X` conversion character in
+[`PatternLayout`](http://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout), for
+example, `%X{opencensusTraceId}`.
+
+See an example Log4j configuration file in the demo:
+https://github.com/census-ecosystem/opencensus-experiments/tree/master/java/log_correlation/log4j2/src/main/resources/log4j2.xml
+
+### Java Versions
+
+Java 6 or above is required for using this artifact.
diff --git a/contrib/log_correlation/log4j2/build.gradle b/contrib/log_correlation/log4j2/build.gradle
new file mode 100644
index 0000000..4a4a6eb
--- /dev/null
+++ b/contrib/log_correlation/log4j2/build.gradle
@@ -0,0 +1,26 @@
+description = 'OpenCensus Log4j 2 Log Correlation'
+
+apply plugin: 'java'
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.log4j2
+
+    testCompile libraries.guava
+
+    signature "org.codehaus.mojo.signature:java16:+@signature"
+}
+
+compileTestJava {
+    sourceCompatibility = "1.7"
+    targetCompatibility = "1.7"
+}
+
+test {
+    systemProperties['log4j2.contextDataInjector'] =
+        'io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector'
+
+    // Each test class should run in a separate JVM. See the comment in
+    // AbstractOpenCensusLog4jLogCorrelationTest.
+    forkEvery = 1
+}
diff --git a/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/ContextDataUtils.java b/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/ContextDataUtils.java
new file mode 100644
index 0000000..dd32e44
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/ContextDataUtils.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2018, 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.logcorrelation.log4j2;
+
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.unsafe.ContextUtils;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
+import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.apache.logging.log4j.util.TriConsumer;
+
+// Implementation of the methods inherited from ContextDataInjector.
+//
+// This class uses "shareable" to mean that a method's return value can be passed to another
+// thread.
+final class ContextDataUtils {
+  private ContextDataUtils() {}
+
+  // The implementation of this method is based on the example in the Javadocs for
+  // ContextDataInjector.injectContextData.
+  static StringMap injectContextData(
+      SpanSelection spanSelection, @Nullable List<Property> properties, StringMap reusable) {
+    if (properties == null || properties.isEmpty()) {
+      return shareableRawContextData(spanSelection);
+    }
+    // Context data has precedence over configuration properties.
+    putProperties(properties, reusable);
+    // TODO(sebright): The following line can be optimized. See
+    //     https://github.com/census-instrumentation/opencensus-java/pull/1422/files#r216425494.
+    reusable.putAll(nonShareableRawContextData(spanSelection));
+    return reusable;
+  }
+
+  private static void putProperties(Collection<Property> properties, StringMap stringMap) {
+    for (Property property : properties) {
+      stringMap.putValue(property.getName(), property.getValue());
+    }
+  }
+
+  private static StringMap shareableRawContextData(SpanSelection spanSelection) {
+    SpanContext spanContext = shouldAddTracingDataToLogEvent(spanSelection);
+    return spanContext == null
+        ? getShareableContextData()
+        : getShareableContextAndTracingData(spanContext);
+  }
+
+  static ReadOnlyStringMap nonShareableRawContextData(SpanSelection spanSelection) {
+    SpanContext spanContext = shouldAddTracingDataToLogEvent(spanSelection);
+    return spanContext == null
+        ? getNonShareableContextData()
+        : getShareableContextAndTracingData(spanContext);
+  }
+
+  // This method returns the current span context iff tracing data should be added to the LogEvent.
+  // It avoids getting the current span when the feature is disabled, for efficiency.
+  @Nullable
+  private static SpanContext shouldAddTracingDataToLogEvent(SpanSelection spanSelection) {
+    switch (spanSelection) {
+      case NO_SPANS:
+        return null;
+      case SAMPLED_SPANS:
+        SpanContext spanContext = getCurrentSpanContext();
+        if (spanContext.getTraceOptions().isSampled()) {
+          return spanContext;
+        } else {
+          return null;
+        }
+      case ALL_SPANS:
+        return getCurrentSpanContext();
+    }
+    throw new AssertionError("Unknown spanSelection: " + spanSelection);
+  }
+
+  private static StringMap getShareableContextData() {
+    ReadOnlyThreadContextMap context = ThreadContext.getThreadContextMap();
+
+    // Return a new object, since StringMap is modifiable.
+    return context == null
+        ? new SortedArrayStringMap(ThreadContext.getImmutableContext())
+        : new SortedArrayStringMap(context.getReadOnlyContextData());
+  }
+
+  private static ReadOnlyStringMap getNonShareableContextData() {
+    ReadOnlyThreadContextMap context = ThreadContext.getThreadContextMap();
+    if (context != null) {
+      return context.getReadOnlyContextData();
+    } else {
+      Map<String, String> contextMap = ThreadContext.getImmutableContext();
+      return contextMap.isEmpty()
+          ? UnmodifiableReadOnlyStringMap.EMPTY
+          : new UnmodifiableReadOnlyStringMap(contextMap);
+    }
+  }
+
+  private static StringMap getShareableContextAndTracingData(SpanContext spanContext) {
+    ReadOnlyThreadContextMap context = ThreadContext.getThreadContextMap();
+    SortedArrayStringMap stringMap;
+    if (context == null) {
+      stringMap = new SortedArrayStringMap(ThreadContext.getImmutableContext());
+    } else {
+      StringMap contextData = context.getReadOnlyContextData();
+      stringMap = new SortedArrayStringMap(contextData.size() + 3);
+      stringMap.putAll(contextData);
+    }
+    // TODO(sebright): Move the calls to TraceId.toLowerBase16() and SpanId.toLowerBase16() out of
+    // the critical path by wrapping the trace and span IDs in objects that call toLowerBase16() in
+    // their toString() methods, after there is a fix for
+    // https://github.com/census-instrumentation/opencensus-java/issues/1436.
+    stringMap.putValue(
+        OpenCensusTraceContextDataInjector.TRACE_ID_CONTEXT_KEY,
+        spanContext.getTraceId().toLowerBase16());
+    stringMap.putValue(
+        OpenCensusTraceContextDataInjector.SPAN_ID_CONTEXT_KEY,
+        spanContext.getSpanId().toLowerBase16());
+    stringMap.putValue(
+        OpenCensusTraceContextDataInjector.TRACE_SAMPLED_CONTEXT_KEY,
+        spanContext.getTraceOptions().isSampled() ? "true" : "false");
+    return stringMap;
+  }
+
+  private static SpanContext getCurrentSpanContext() {
+    Span span = ContextUtils.CONTEXT_SPAN_KEY.get();
+    return span == null ? SpanContext.INVALID : span.getContext();
+  }
+
+  @Immutable
+  private static final class UnmodifiableReadOnlyStringMap implements ReadOnlyStringMap {
+    private static final long serialVersionUID = 0L;
+
+    static final ReadOnlyStringMap EMPTY =
+        new UnmodifiableReadOnlyStringMap(Collections.<String, String>emptyMap());
+
+    private final Map<String, String> map;
+
+    UnmodifiableReadOnlyStringMap(Map<String, String> map) {
+      this.map = map;
+    }
+
+    @Override
+    public boolean containsKey(String key) {
+      return map.containsKey(key);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <V> void forEach(BiConsumer<String, ? super V> action) {
+      for (Entry<String, String> entry : map.entrySet()) {
+        action.accept(entry.getKey(), (V) entry.getValue());
+      }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <V, S> void forEach(TriConsumer<String, ? super V, S> action, S state) {
+      for (Entry<String, String> entry : map.entrySet()) {
+        action.accept(entry.getKey(), (V) entry.getValue(), state);
+      }
+    }
+
+    @Override
+    @Nullable
+    @SuppressWarnings({
+      "unchecked",
+      "TypeParameterUnusedInFormals" // This is an overridden method.
+    })
+    public <V> V getValue(String key) {
+      return (V) map.get(key);
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return map.isEmpty();
+    }
+
+    @Override
+    public int size() {
+      return map.size();
+    }
+
+    @Override
+    public Map<String, String> toMap() {
+      return map;
+    }
+  }
+}
diff --git a/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjector.java b/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjector.java
new file mode 100644
index 0000000..38b1882
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjector.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2018, 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.logcorrelation.log4j2;
+
+import io.opencensus.common.ExperimentalApi;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.logging.log4j.core.ContextDataInjector;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.StringMap;
+
+/**
+ * A Log4j {@link ContextDataInjector} that adds OpenCensus tracing data to log events.
+ *
+ * <p>This class adds the following key-value pairs:
+ *
+ * <ul>
+ *   <li>{@value #TRACE_ID_CONTEXT_KEY} - the lowercase base16 encoding of the current trace ID
+ *   <li>{@value #SPAN_ID_CONTEXT_KEY} - the lowercase base16 encoding of the current span ID
+ *   <li>{@value #TRACE_SAMPLED_CONTEXT_KEY} - the sampling decision of the current span ({@code
+ *       "true"} or {@code "false"})
+ * </ul>
+ *
+ * <p>The tracing data can be accessed with {@link LogEvent#getContextData} or included in a {@link
+ * Layout}. For example, the following patterns could be used to include the tracing data with a <a
+ * href="https://logging.apache.org/log4j/2.x/manual/layouts.html#Pattern_Layout">Pattern
+ * Layout</a>:
+ *
+ * <ul>
+ *   <li><code>%X{opencensusTraceId}</code>
+ *   <li><code>%X{opencensusSpanId}</code>
+ *   <li><code>%X{opencensusTraceSampled}</code>
+ * </ul>
+ *
+ * <p>This feature is currently experimental.
+ *
+ * @since 0.16
+ * @see <a
+ *     href="https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/ContextDataInjector.html">org.apache.logging.log4j.core.ContextDataInjector</a>
+ */
+@ExperimentalApi
+public final class OpenCensusTraceContextDataInjector implements ContextDataInjector {
+  private static final SpanSelection DEFAULT_SPAN_SELECTION = SpanSelection.ALL_SPANS;
+
+  /**
+   * Context key for the current trace ID. The name is {@value}.
+   *
+   * @since 0.16
+   */
+  public static final String TRACE_ID_CONTEXT_KEY = "opencensusTraceId";
+
+  /**
+   * Context key for the current span ID. The name is {@value}.
+   *
+   * @since 0.16
+   */
+  public static final String SPAN_ID_CONTEXT_KEY = "opencensusSpanId";
+
+  /**
+   * Context key for the sampling decision of the current span. The name is {@value}.
+   *
+   * @since 0.16
+   */
+  public static final String TRACE_SAMPLED_CONTEXT_KEY = "opencensusTraceSampled";
+
+  /**
+   * Name of the property that defines the {@link SpanSelection}. The name is {@value}.
+   *
+   * @since 0.16
+   */
+  public static final String SPAN_SELECTION_PROPERTY_NAME =
+      "io.opencensus.contrib.logcorrelation.log4j2."
+          + "OpenCensusTraceContextDataInjector.spanSelection";
+
+  private final SpanSelection spanSelection;
+
+  /**
+   * How to decide whether to add tracing data from the current span to a log entry.
+   *
+   * @since 0.16
+   */
+  public enum SpanSelection {
+
+    /**
+     * Never add tracing data to log entries. This constant disables the log correlation feature.
+     *
+     * @since 0.16
+     */
+    NO_SPANS,
+
+    /**
+     * Add tracing data to a log entry iff the current span is sampled.
+     *
+     * @since 0.16
+     */
+    SAMPLED_SPANS,
+
+    /**
+     * Always add tracing data to log entries, even when the current span is not sampled. This is
+     * the default.
+     *
+     * @since 0.16
+     */
+    ALL_SPANS
+  }
+
+  /**
+   * Returns the {@code SpanSelection} setting for this instance.
+   *
+   * @return the {@code SpanSelection} setting for this instance.
+   * @since 0.16
+   */
+  public SpanSelection getSpanSelection() {
+    return spanSelection;
+  }
+
+  /**
+   * Constructor to be called by Log4j.
+   *
+   * <p>This constructor looks up the {@link SpanSelection} using the system property {@link
+   * #SPAN_SELECTION_PROPERTY_NAME}.
+   *
+   * @since 0.16
+   */
+  public OpenCensusTraceContextDataInjector() {
+    this(lookUpSpanSelectionProperty());
+  }
+
+  // visible for testing
+  OpenCensusTraceContextDataInjector(SpanSelection spanSelection) {
+    this.spanSelection = spanSelection;
+  }
+
+  private static SpanSelection lookUpSpanSelectionProperty() {
+    String spanSelectionProperty = System.getProperty(SPAN_SELECTION_PROPERTY_NAME);
+    return spanSelectionProperty == null || spanSelectionProperty.isEmpty()
+        ? DEFAULT_SPAN_SELECTION
+        : parseSpanSelection(spanSelectionProperty);
+  }
+
+  private static SpanSelection parseSpanSelection(String spanSelection) {
+    try {
+      return SpanSelection.valueOf(spanSelection);
+    } catch (IllegalArgumentException e) {
+      return DEFAULT_SPAN_SELECTION;
+    }
+  }
+
+  // Note that this method must return an object that can be passed to another thread.
+  @Override
+  public StringMap injectContextData(@Nullable List<Property> properties, StringMap reusable) {
+    return ContextDataUtils.injectContextData(spanSelection, properties, reusable);
+  }
+
+  // Note that this method does not need to return an object that can be passed to another thread.
+  @Override
+  public ReadOnlyStringMap rawContextData() {
+    return ContextDataUtils.nonShareableRawContextData(spanSelection);
+  }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/AbstractOpenCensusLog4jLogCorrelationTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/AbstractOpenCensusLog4jLogCorrelationTest.java
new file mode 100644
index 0000000..93ad85e
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/AbstractOpenCensusLog4jLogCorrelationTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2018, 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.logcorrelation.log4j2;
+
+import io.opencensus.common.Function;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.Tracing;
+import java.io.StringWriter;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.StringLayout;
+import org.apache.logging.log4j.core.appender.WriterAppender;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+
+/**
+ * Superclass for all Log4j log correlation test classes.
+ *
+ * <p>The tests are split into multiple classes so that each one can be run with a different value
+ * for the system property {@link OpenCensusTraceContextDataInjector#SPAN_SELECTION_PROPERTY_NAME}.
+ * The property must be set when Log4j initializes a static variable, and running each test class in
+ * a separate JVM causes the static variable to be reinitialized.
+ */
+abstract class AbstractOpenCensusLog4jLogCorrelationTest {
+  private static final Tracer tracer = Tracing.getTracer();
+
+  static final String TEST_PATTERN =
+      "traceId=%X{opencensusTraceId} spanId=%X{opencensusSpanId} "
+          + "sampled=%X{opencensusTraceSampled} %-5level - %msg";
+
+  static final Tracestate EMPTY_TRACESTATE = Tracestate.builder().build();
+
+  private static Logger logger;
+
+  // This method initializes Log4j after setting the SpanSelection, which means that Log4j
+  // initializes a static variable with a ContextDataInjector that is constructed with the proper
+  // SpanSelection. This method should be called from a @BeforeClass method in each subclass.
+  static void initializeLog4j(SpanSelection spanSelection) {
+    System.setProperty(
+        OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME, spanSelection.toString());
+    logger = (Logger) LogManager.getLogger(AbstractOpenCensusLog4jLogCorrelationTest.class);
+  }
+
+  // Reconfigures Log4j using the given arguments and runs the function with the given SpanContext
+  // in scope.
+  String logWithSpanAndLog4jConfiguration(
+      String log4jPattern, SpanContext spanContext, Function<Logger, Void> loggingFunction) {
+    StringWriter output = new StringWriter();
+    StringLayout layout = PatternLayout.newBuilder().withPattern(log4jPattern).build();
+    Appender appender =
+        WriterAppender.newBuilder()
+            .setTarget(output)
+            .setLayout(layout)
+            .setName("TestAppender")
+            .build();
+    ((LoggerContext) LogManager.getContext(false)).updateLoggers();
+    appender.start();
+    logger.addAppender(appender);
+    logger.setLevel(Level.ALL);
+    try {
+      logWithSpan(spanContext, loggingFunction, logger);
+      return output.toString();
+    } finally {
+      logger.removeAppender(appender);
+    }
+  }
+
+  private static void logWithSpan(
+      SpanContext spanContext, Function<Logger, Void> loggingFunction, Logger logger) {
+    Scope scope = tracer.withSpan(new TestSpan(spanContext));
+    try {
+      loggingFunction.apply(logger);
+    } finally {
+      scope.close();
+    }
+  }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationAllSpansTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationAllSpansTest.java
new file mode 100644
index 0000000..355c9b6
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationAllSpansTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2018, 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.logcorrelation.log4j2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Function;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Log4j log correlation with {@link
+ * OpenCensusTraceContextDataInjector#SPAN_SELECTION_PROPERTY_NAME} set to {@link
+ * SpanSelection#ALL_SPANS}.
+ */
+@RunWith(JUnit4.class)
+public final class OpenCensusLog4jLogCorrelationAllSpansTest
+    extends AbstractOpenCensusLog4jLogCorrelationTest {
+
+  @BeforeClass
+  public static void setUp() {
+    initializeLog4j(SpanSelection.ALL_SPANS);
+  }
+
+  @Test
+  public void addSampledSpanToLogEntryWithAllSpans() {
+    String log =
+        logWithSpanAndLog4jConfiguration(
+            TEST_PATTERN,
+            SpanContext.create(
+                TraceId.fromLowerBase16("b9718fe3d82d36fce0e6a1ada1c21db0"),
+                SpanId.fromLowerBase16("75159dde8c503fee"),
+                TraceOptions.builder().setIsSampled(true).build(),
+                EMPTY_TRACESTATE),
+            new Function<Logger, Void>() {
+              @Override
+              public Void apply(Logger logger) {
+                logger.warn("message #1");
+                return null;
+              }
+            });
+    assertThat(log)
+        .isEqualTo(
+            "traceId=b9718fe3d82d36fce0e6a1ada1c21db0 spanId=75159dde8c503fee "
+                + "sampled=true WARN  - message #1");
+  }
+
+  @Test
+  public void addNonSampledSpanToLogEntryWithAllSpans() {
+    String log =
+        logWithSpanAndLog4jConfiguration(
+            TEST_PATTERN,
+            SpanContext.create(
+                TraceId.fromLowerBase16("cd7061dfa9d312cdcc42edab3feab51b"),
+                SpanId.fromLowerBase16("117d42d4c7acd066"),
+                TraceOptions.builder().setIsSampled(false).build(),
+                EMPTY_TRACESTATE),
+            new Function<Logger, Void>() {
+              @Override
+              public Void apply(Logger logger) {
+                logger.info("message #2");
+                return null;
+              }
+            });
+    assertThat(log)
+        .isEqualTo(
+            "traceId=cd7061dfa9d312cdcc42edab3feab51b spanId=117d42d4c7acd066 sampled=false INFO  "
+                + "- message #2");
+  }
+
+  @Test
+  public void addBlankSpanToLogEntryWithAllSpans() {
+    String log =
+        logWithSpanAndLog4jConfiguration(
+            TEST_PATTERN,
+            SpanContext.INVALID,
+            new Function<Logger, Void>() {
+              @Override
+              public Void apply(Logger logger) {
+                logger.fatal("message #3");
+                return null;
+              }
+            });
+    assertThat(log)
+        .isEqualTo(
+            "traceId=00000000000000000000000000000000 spanId=0000000000000000 sampled=false FATAL "
+                + "- message #3");
+  }
+
+  @Test
+  public void preserveOtherKeyValuePairs() {
+    String log =
+        logWithSpanAndLog4jConfiguration(
+            "%X{opencensusTraceId} %X{myTestKey} %-5level - %msg",
+            SpanContext.create(
+                TraceId.fromLowerBase16("c95329bb6b7de41afbc51a231c128f97"),
+                SpanId.fromLowerBase16("bf22ea74d38eddad"),
+                TraceOptions.builder().setIsSampled(true).build(),
+                EMPTY_TRACESTATE),
+            new Function<Logger, Void>() {
+              @Override
+              public Void apply(Logger logger) {
+                String key = "myTestKey";
+                ThreadContext.put(key, "myTestValue");
+                try {
+                  logger.error("message #4");
+                } finally {
+                  ThreadContext.remove(key);
+                }
+                return null;
+              }
+            });
+    assertThat(log).isEqualTo("c95329bb6b7de41afbc51a231c128f97 myTestValue ERROR - message #4");
+  }
+
+  @Test
+  public void overwriteExistingTracingKey() {
+    String log =
+        logWithSpanAndLog4jConfiguration(
+            TEST_PATTERN,
+            SpanContext.create(
+                TraceId.fromLowerBase16("18e4ae44273a0c44e0c9ea4380792c66"),
+                SpanId.fromLowerBase16("199a7e16daa000a7"),
+                TraceOptions.builder().setIsSampled(true).build(),
+                EMPTY_TRACESTATE),
+            new Function<Logger, Void>() {
+              @Override
+              public Void apply(Logger logger) {
+                ThreadContext.put(
+                    OpenCensusTraceContextDataInjector.TRACE_ID_CONTEXT_KEY, "existingTraceId");
+                try {
+                  logger.error("message #5");
+                } finally {
+                  ThreadContext.remove(OpenCensusTraceContextDataInjector.TRACE_ID_CONTEXT_KEY);
+                }
+                return null;
+              }
+            });
+    assertThat(log)
+        .isEqualTo(
+            "traceId=18e4ae44273a0c44e0c9ea4380792c66 spanId=199a7e16daa000a7 "
+                + "sampled=true ERROR - message #5");
+  }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationNoSpansTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationNoSpansTest.java
new file mode 100644
index 0000000..1205924
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationNoSpansTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018, 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.logcorrelation.log4j2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Function;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import org.apache.logging.log4j.core.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Log4j log correlation with {@link
+ * OpenCensusTraceContextDataInjector#SPAN_SELECTION_PROPERTY_NAME} set to {@link
+ * SpanSelection#NO_SPANS}.
+ */
+@RunWith(JUnit4.class)
+public final class OpenCensusLog4jLogCorrelationNoSpansTest
+    extends AbstractOpenCensusLog4jLogCorrelationTest {
+
+  @BeforeClass
+  public static void setUp() {
+    initializeLog4j(SpanSelection.NO_SPANS);
+  }
+
+  @Test
+  public void doNotAddSampledSpanToLogEntryWithNoSpans() {
+    String log =
+        logWithSpanAndLog4jConfiguration(
+            TEST_PATTERN,
+            SpanContext.create(
+                TraceId.fromLowerBase16("03d2ada98f6eb8330605a45a88c7e67d"),
+                SpanId.fromLowerBase16("ce5b1cf09fe58bcb"),
+                TraceOptions.builder().setIsSampled(true).build(),
+                EMPTY_TRACESTATE),
+            new Function<Logger, Void>() {
+              @Override
+              public Void apply(Logger logger) {
+                logger.trace("message #1");
+                return null;
+              }
+            });
+    assertThat(log).isEqualTo("traceId= spanId= sampled= TRACE - message #1");
+  }
+
+  @Test
+  public void doNotAddNonSampledSpanToLogEntryWithNoSpans() {
+    String log =
+        logWithSpanAndLog4jConfiguration(
+            TEST_PATTERN,
+            SpanContext.create(
+                TraceId.fromLowerBase16("09664283d189791de5218ffe3be88d54"),
+                SpanId.fromLowerBase16("a7203a50089a4029"),
+                TraceOptions.builder().setIsSampled(false).build(),
+                EMPTY_TRACESTATE),
+            new Function<Logger, Void>() {
+              @Override
+              public Void apply(Logger logger) {
+                logger.warn("message #2");
+                return null;
+              }
+            });
+    assertThat(log).isEqualTo("traceId= spanId= sampled= WARN  - message #2");
+  }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationSampledSpansTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationSampledSpansTest.java
new file mode 100644
index 0000000..bbce413
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationSampledSpansTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018, 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.logcorrelation.log4j2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Function;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import org.apache.logging.log4j.core.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Log4j log correlation with {@link
+ * OpenCensusTraceContextDataInjector#SPAN_SELECTION_PROPERTY_NAME} set to {@link
+ * SpanSelection#SAMPLED_SPANS}.
+ */
+@RunWith(JUnit4.class)
+public final class OpenCensusLog4jLogCorrelationSampledSpansTest
+    extends AbstractOpenCensusLog4jLogCorrelationTest {
+
+  @BeforeClass
+  public static void setUp() {
+    initializeLog4j(SpanSelection.SAMPLED_SPANS);
+  }
+
+  @Test
+  public void addSampledSpanToLogEntryWithSampledSpans() {
+    String log =
+        logWithSpanAndLog4jConfiguration(
+            TEST_PATTERN,
+            SpanContext.create(
+                TraceId.fromLowerBase16("0af7a7bef890695f1c5e85a8e7290164"),
+                SpanId.fromLowerBase16("d3f07c467ec2fbb2"),
+                TraceOptions.builder().setIsSampled(true).build(),
+                EMPTY_TRACESTATE),
+            new Function<Logger, Void>() {
+              @Override
+              public Void apply(Logger logger) {
+                logger.error("message #1");
+                return null;
+              }
+            });
+    assertThat(log)
+        .isEqualTo(
+            "traceId=0af7a7bef890695f1c5e85a8e7290164 spanId=d3f07c467ec2fbb2 sampled=true ERROR "
+                + "- message #1");
+  }
+
+  @Test
+  public void doNotAddNonSampledSpanToLogEntryWithSampledSpans() {
+    String log =
+        logWithSpanAndLog4jConfiguration(
+            TEST_PATTERN,
+            SpanContext.create(
+                TraceId.fromLowerBase16("9e09b559ebb8f7f7ed7451aff68cf441"),
+                SpanId.fromLowerBase16("0fc9ef54c50a1816"),
+                TraceOptions.builder().setIsSampled(false).build(),
+                EMPTY_TRACESTATE),
+            new Function<Logger, Void>() {
+              @Override
+              public Void apply(Logger logger) {
+                logger.debug("message #2");
+                return null;
+              }
+            });
+    assertThat(log).isEqualTo("traceId= spanId= sampled= DEBUG - message #2");
+  }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjectorTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjectorTest.java
new file mode 100644
index 0000000..3b70405
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjectorTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2018, 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.logcorrelation.log4j2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.Tracing;
+import java.util.Collections;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link OpenCensusTraceContextDataInjector}. */
+@RunWith(JUnit4.class)
+public final class OpenCensusTraceContextDataInjectorTest {
+  static final Tracestate EMPTY_TRACESTATE = Tracestate.builder().build();
+
+  private final Tracer tracer = Tracing.getTracer();
+
+  @Test
+  @SuppressWarnings("TruthConstantAsserts")
+  public void spanSelectionPropertyName() {
+    assertThat(OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME)
+        .isEqualTo(OpenCensusTraceContextDataInjector.class.getName() + ".spanSelection");
+  }
+
+  @Test
+  public void traceIdKey() {
+    assertThat(OpenCensusTraceContextDataInjector.TRACE_ID_CONTEXT_KEY)
+        .isEqualTo("opencensusTraceId");
+  }
+
+  @Test
+  public void spanIdKey() {
+    assertThat(OpenCensusTraceContextDataInjector.SPAN_ID_CONTEXT_KEY)
+        .isEqualTo("opencensusSpanId");
+  }
+
+  @Test
+  public void traceSampledKey() {
+    assertThat(OpenCensusTraceContextDataInjector.TRACE_SAMPLED_CONTEXT_KEY)
+        .isEqualTo("opencensusTraceSampled");
+  }
+
+  @Test
+  public void spanSelectionDefaultIsAllSpans() {
+    assertThat(new OpenCensusTraceContextDataInjector().getSpanSelection())
+        .isEqualTo(SpanSelection.ALL_SPANS);
+  }
+
+  @Test
+  public void setSpanSelectionWithSystemProperty() {
+    try {
+      System.setProperty(
+          OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME, "NO_SPANS");
+      assertThat(new OpenCensusTraceContextDataInjector().getSpanSelection())
+          .isEqualTo(SpanSelection.NO_SPANS);
+    } finally {
+      System.clearProperty(OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME);
+    }
+  }
+
+  @Test
+  public void useDefaultValueForInvalidSpanSelection() {
+    try {
+      System.setProperty(
+          OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME,
+          "INVALID_SPAN_SELECTION");
+      assertThat(new OpenCensusTraceContextDataInjector().getSpanSelection())
+          .isEqualTo(SpanSelection.ALL_SPANS);
+    } finally {
+      System.clearProperty(OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME);
+    }
+  }
+
+  @Test
+  public void insertConfigurationProperties() {
+    assertThat(
+            new OpenCensusTraceContextDataInjector(SpanSelection.ALL_SPANS)
+                .injectContextData(
+                    Lists.newArrayList(
+                        Property.createProperty("property1", "value1"),
+                        Property.createProperty("property2", "value2")),
+                    new SortedArrayStringMap())
+                .toMap())
+        .containsExactly(
+            "property1",
+            "value1",
+            "property2",
+            "value2",
+            "opencensusTraceId",
+            "00000000000000000000000000000000",
+            "opencensusSpanId",
+            "0000000000000000",
+            "opencensusTraceSampled",
+            "false");
+  }
+
+  @Test
+  public void handleEmptyConfigurationProperties() {
+    assertContainsOnlyDefaultTracingEntries(
+        new OpenCensusTraceContextDataInjector(SpanSelection.ALL_SPANS)
+            .injectContextData(Collections.<Property>emptyList(), new SortedArrayStringMap()));
+  }
+
+  @Test
+  public void handleNullConfigurationProperties() {
+    assertContainsOnlyDefaultTracingEntries(
+        new OpenCensusTraceContextDataInjector(SpanSelection.ALL_SPANS)
+            .injectContextData(null, new SortedArrayStringMap()));
+  }
+
+  private static void assertContainsOnlyDefaultTracingEntries(StringMap stringMap) {
+    assertThat(stringMap.toMap())
+        .containsExactly(
+            "opencensusTraceId",
+            "00000000000000000000000000000000",
+            "opencensusSpanId",
+            "0000000000000000",
+            "opencensusTraceSampled",
+            "false");
+  }
+
+  @Test
+  public void rawContextDataWithTracingData() {
+    OpenCensusTraceContextDataInjector plugin =
+        new OpenCensusTraceContextDataInjector(SpanSelection.ALL_SPANS);
+    SpanContext spanContext =
+        SpanContext.create(
+            TraceId.fromLowerBase16("e17944156660f55b8cae5ce3f45d4a40"),
+            SpanId.fromLowerBase16("fc3d2ba0d283b66a"),
+            TraceOptions.builder().setIsSampled(true).build(),
+            EMPTY_TRACESTATE);
+    Scope scope = tracer.withSpan(new TestSpan(spanContext));
+    try {
+      String key = "myTestKey";
+      ThreadContext.put(key, "myTestValue");
+      try {
+        assertThat(plugin.rawContextData().toMap())
+            .containsExactly(
+                "myTestKey",
+                "myTestValue",
+                "opencensusTraceId",
+                "e17944156660f55b8cae5ce3f45d4a40",
+                "opencensusSpanId",
+                "fc3d2ba0d283b66a",
+                "opencensusTraceSampled",
+                "true");
+      } finally {
+        ThreadContext.remove(key);
+      }
+    } finally {
+      scope.close();
+    }
+  }
+
+  @Test
+  public void rawContextDataWithoutTracingData() {
+    OpenCensusTraceContextDataInjector plugin =
+        new OpenCensusTraceContextDataInjector(SpanSelection.NO_SPANS);
+    SpanContext spanContext =
+        SpanContext.create(
+            TraceId.fromLowerBase16("ea236000f6d387fe7c06c5a6d6458b53"),
+            SpanId.fromLowerBase16("f3b39dbbadb73074"),
+            TraceOptions.builder().setIsSampled(true).build(),
+            EMPTY_TRACESTATE);
+    Scope scope = tracer.withSpan(new TestSpan(spanContext));
+    try {
+      String key = "myTestKey";
+      ThreadContext.put(key, "myTestValue");
+      try {
+        assertThat(plugin.rawContextData().toMap()).containsExactly("myTestKey", "myTestValue");
+      } finally {
+        ThreadContext.remove(key);
+      }
+    } finally {
+      scope.close();
+    }
+  }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/TestSpan.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/TestSpan.java
new file mode 100644
index 0000000..7af4606
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/TestSpan.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018, 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.logcorrelation.log4j2;
+
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import java.util.EnumSet;
+import java.util.Map;
+
+// Simple test Span that holds a SpanContext. The tests cannot use Span directly, since it is
+// abstract.
+final class TestSpan extends Span {
+  TestSpan(SpanContext context) {
+    super(context, EnumSet.of(Options.RECORD_EVENTS));
+  }
+
+  @Override
+  public void end(EndSpanOptions options) {}
+
+  @Override
+  public void addLink(Link link) {}
+
+  @Override
+  public void addAnnotation(Annotation annotation) {}
+
+  @Override
+  public void addAnnotation(String description, Map<String, AttributeValue> attributes) {}
+}
diff --git a/contrib/log_correlation/stackdriver/README.md b/contrib/log_correlation/stackdriver/README.md
new file mode 100644
index 0000000..8d99ff2
--- /dev/null
+++ b/contrib/log_correlation/stackdriver/README.md
@@ -0,0 +1,147 @@
+# OpenCensus Stackdriver Log Correlation
+
+This subproject is currently experimental, so it may be redesigned or removed in the future.  It
+will remain experimental until we have a specification for a log correlation feature in
+[opencensus-specs](https://github.com/census-instrumentation/opencensus-specs/)
+(issue [#123](https://github.com/census-instrumentation/opencensus-specs/issues/123)).
+
+The `opencensus-contrib-log-correlation-stackdriver` artifact provides a
+[Stackdriver Logging](https://cloud.google.com/logging/)
+[`LoggingEnhancer`](http://googlecloudplatform.github.io/google-cloud-java/google-cloud-clients/apidocs/com/google/cloud/logging/LoggingEnhancer.html)
+that automatically adds tracing data to log entries. The class name is
+`OpenCensusTraceLoggingEnhancer`. `OpenCensusTraceLoggingEnhancer` adds the current trace and span
+ID to each log entry, which allows Stackdriver to display the log entries associated with each
+trace, or filter logs based on trace or span ID. It currently also adds the sampling decision using
+the label "`opencensusTraceSampled`".
+
+## Instructions
+
+### Prerequisites
+
+This log correlation feature requires a project that is using the
+[`com.google.cloud:google-cloud-logging`](https://github.com/GoogleCloudPlatform/google-cloud-java/tree/master/google-cloud-clients/google-cloud-logging)
+library to export logs to Stackdriver. `google-cloud-logging` must be version `1.33.0` or later.
+The application can run on Google Cloud Platform, on-premise, or on
+another cloud platform. See https://cloud.google.com/logging/docs/setup/java for instructions for
+setting up `google-cloud-logging`.
+
+**Note that this artifact does not support logging done through the Stackdriver Logging agent.**
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-log-correlation-stackdriver</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+runtime 'io.opencensus:opencensus-contrib-log-correlation-stackdriver:0.16.1'
+```
+
+### Configure the `OpenCensusTraceLoggingEnhancer`
+
+#### Setting the project ID
+
+By default, `OpenCensusTraceLoggingEnhancer` looks up the project ID from `google-cloud-java`. See
+[here](https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id) for
+instructions for configuring the project ID with `google-cloud-java`.
+
+To override the project ID, set the following property as a system property or as a
+`java.util.logging` property:
+
+`io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.projectId`
+
+#### Choosing when to add tracing data to log entries
+
+The following property controls the decision to add tracing data from the current span to a log
+entry:
+
+`io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.spanSelection`
+
+The allowed values are:
+
+* `ALL_SPANS`: adds tracing data to all log entries (default)
+
+* `NO_SPANS`: disables the log correlation feature
+
+* `SAMPLED_SPANS`: adds tracing data to log entries when the current span is sampled
+
+Other aspects of configuring the `OpenCensusTraceLoggingEnhancer` depend on the logging
+implementation and `google-cloud-logging` adapter in use.
+
+#### Logback with `google-cloud-logging-logback` `LoggingAppender`
+
+The `LoggingAppender` should already be configured in `logback.xml` as described in
+https://cloud.google.com/logging/docs/setup/java#logback_appender. Add
+"`io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer`" to the list of
+enhancers. Optionally, set the `spanSelection` and `projectId` properties described above as system
+properties.
+
+Here is an example `logback.xml`, based on the
+[`google-cloud-logging-logback` example](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/a2b04b20d81ee631439a9368fb99b44849519e28/logging/logback/src/main/resources/logback.xml).
+It specifies the `LoggingEnhancer` class and sets both optional properties:
+
+```xml
+<configuration>
+  <property scope="system" name="io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.spanSelection" value="SAMPLED_SPANS" />
+  <property scope="system" name="io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.projectId" value="my-project-id" />
+  <appender name="CLOUD" class="com.google.cloud.logging.logback.LoggingAppender">
+    <enhancer>io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer</enhancer>
+  </appender>
+
+  <root level="info">
+    <appender-ref ref="CLOUD" />
+  </root>
+</configuration>
+```
+
+See
+https://github.com/census-ecosystem/opencensus-experiments/tree/master/java/log_correlation/stackdriver/logback
+for a full example.
+
+#### `java.util.logging` with `google-cloud-logging` `LoggingHandler`
+
+The `LoggingHandler` should already be configured in a logging `.properties` file, as described in
+https://cloud.google.com/logging/docs/setup/java#jul_handler. Add
+"`io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer`" to the list of
+enhancers. Optionally, set the `spanSelection` and `projectId` properties described above in the
+properties file.
+
+Here is an example `.properties` file, based on the
+[`google-cloud-logging` example](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/a2b04b20d81ee631439a9368fb99b44849519e28/logging/jul/src/main/resources/logging.properties).
+It specifies the `LoggingEnhancer` class and sets both optional properties:
+
+```properties
+.level = INFO
+
+com.example.MyClass.handlers=com.google.cloud.logging.LoggingHandler
+
+com.google.cloud.logging.LoggingHandler.enhancers=io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer
+io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.spanSelection=SAMPLED_SPANS
+io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.projectId=my-project-id
+```
+
+See
+https://github.com/census-ecosystem/opencensus-experiments/tree/master/java/log_correlation/stackdriver/java_util_logging
+for a full example.
+
+#### Custom `google-cloud-logging` adapter
+
+The `google-cloud-logging` adapter needs to instantiate the `OpenCensusTraceLoggingEnhancer`,
+possibly by looking up the class name of the `LoggingEnhancer` in a configuration file and
+instantiating it with reflection. Then the adapter needs to call the `LoggingEnhancer`'s
+`enhanceLogEntry` method on all `LogEntry`s that will be passed to `google-cloud-logging`'s
+`Logging.write` method. `enhanceLogEntry` must be called in the same thread that executed the log
+statement, in order to provide the current trace and span ID.
+
+#### Java Versions
+
+Java 7 or above is required for using this artifact.
diff --git a/contrib/log_correlation/stackdriver/build.gradle b/contrib/log_correlation/stackdriver/build.gradle
new file mode 100644
index 0000000..4d8a298
--- /dev/null
+++ b/contrib/log_correlation/stackdriver/build.gradle
@@ -0,0 +1,13 @@
+description = 'OpenCensus Stackdriver Log Correlation'
+
+apply plugin: 'java'
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.google_cloud_logging
+
+    testCompile libraries.guava
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/log_correlation/stackdriver/src/main/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancer.java b/contrib/log_correlation/stackdriver/src/main/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancer.java
new file mode 100644
index 0000000..5c3e21f
--- /dev/null
+++ b/contrib/log_correlation/stackdriver/src/main/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancer.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2018, 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.logcorrelation.stackdriver;
+
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.logging.LogEntry;
+import com.google.cloud.logging.LoggingEnhancer;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.unsafe.ContextUtils;
+import java.util.logging.LogManager;
+import javax.annotation.Nullable;
+
+/**
+ * Stackdriver {@link LoggingEnhancer} that adds OpenCensus tracing data to log entries.
+ *
+ * <p>This feature is currently experimental.
+ *
+ * @since 0.15
+ */
+@ExperimentalApi
+public final class OpenCensusTraceLoggingEnhancer implements LoggingEnhancer {
+  private static final String SAMPLED_LABEL_KEY = "opencensusTraceSampled";
+  private static final SpanSelection DEFAULT_SPAN_SELECTION = SpanSelection.ALL_SPANS;
+
+  /**
+   * Name of the property that overrides the default project ID (overrides the value returned by
+   * {@code com.google.cloud.ServiceOptions.getDefaultProjectId()}). The name is {@value}.
+   *
+   * @since 0.15
+   */
+  public static final String PROJECT_ID_PROPERTY_NAME =
+      "io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.projectId";
+
+  /**
+   * Name of the property that defines the {@link SpanSelection}. The name is {@value}.
+   *
+   * @since 0.15
+   */
+  public static final String SPAN_SELECTION_PROPERTY_NAME =
+      "io.opencensus.contrib.logcorrelation.stackdriver."
+          + "OpenCensusTraceLoggingEnhancer.spanSelection";
+
+  private final String projectId;
+  private final SpanSelection spanSelection;
+
+  // This field caches the prefix used for the LogEntry.trace field and is derived from projectId.
+  private final String tracePrefix;
+
+  /**
+   * How to decide whether to add tracing data from the current span to a log entry.
+   *
+   * @since 0.15
+   */
+  public enum SpanSelection {
+
+    /**
+     * Never add tracing data to log entries. This constant disables the log correlation feature.
+     *
+     * @since 0.15
+     */
+    NO_SPANS,
+
+    /**
+     * Add tracing data to a log entry iff the current span is sampled.
+     *
+     * @since 0.15
+     */
+    SAMPLED_SPANS,
+
+    /**
+     * Always add tracing data to log entries, even when the current span is not sampled. This is
+     * the default.
+     *
+     * @since 0.15
+     */
+    ALL_SPANS
+  }
+
+  /**
+   * Constructor to be called by reflection, e.g., by a google-cloud-java {@code LoggingHandler} or
+   * google-cloud-logging-logback {@code LoggingAppender}.
+   *
+   * <p>This constructor looks up the project ID and {@link SpanSelection SpanSelection} from the
+   * environment. It uses the default project ID (the value returned by {@code
+   * com.google.cloud.ServiceOptions.getDefaultProjectId()}), unless the ID is overridden by the
+   * property {@value #PROJECT_ID_PROPERTY_NAME}. It looks up the {@code SpanSelection} using the
+   * property {@value #SPAN_SELECTION_PROPERTY_NAME}. Each property can be specified with a {@link
+   * java.util.logging} property or a system property, with preference given to the logging
+   * property.
+   *
+   * @since 0.15
+   */
+  public OpenCensusTraceLoggingEnhancer() {
+    this(lookUpProjectId(), lookUpSpanSelectionProperty());
+  }
+
+  /**
+   * Constructs a {@code OpenCensusTraceLoggingEnhancer} with the given project ID and {@code
+   * SpanSelection}.
+   *
+   * @param projectId the project ID for this instance.
+   * @param spanSelection the {@code SpanSelection} for this instance.
+   * @since 0.15
+   */
+  public OpenCensusTraceLoggingEnhancer(@Nullable String projectId, SpanSelection spanSelection) {
+    this.projectId = projectId == null ? "" : projectId;
+    this.spanSelection = spanSelection;
+    this.tracePrefix = "projects/" + this.projectId + "/traces/";
+  }
+
+  private static String lookUpProjectId() {
+    String projectIdProperty = lookUpProperty(PROJECT_ID_PROPERTY_NAME);
+    return projectIdProperty == null || projectIdProperty.isEmpty()
+        ? ServiceOptions.getDefaultProjectId()
+        : projectIdProperty;
+  }
+
+  private static SpanSelection lookUpSpanSelectionProperty() {
+    String spanSelectionProperty = lookUpProperty(SPAN_SELECTION_PROPERTY_NAME);
+    return spanSelectionProperty == null || spanSelectionProperty.isEmpty()
+        ? DEFAULT_SPAN_SELECTION
+        : parseSpanSelection(spanSelectionProperty);
+  }
+
+  private static SpanSelection parseSpanSelection(String spanSelection) {
+    try {
+      return SpanSelection.valueOf(spanSelection);
+    } catch (IllegalArgumentException e) {
+      return DEFAULT_SPAN_SELECTION;
+    }
+  }
+
+  // An OpenCensusTraceLoggingEnhancer property can be set with a logging property or a system
+  // property.
+  @Nullable
+  private static String lookUpProperty(String name) {
+    String property = LogManager.getLogManager().getProperty(name);
+    return property == null || property.isEmpty() ? System.getProperty(name) : property;
+  }
+
+  /**
+   * Returns the project ID setting for this instance.
+   *
+   * @return the project ID setting for this instance.
+   * @since 0.15
+   */
+  public String getProjectId() {
+    return projectId;
+  }
+
+  /**
+   * Returns the {@code SpanSelection} setting for this instance.
+   *
+   * @return the {@code SpanSelection} setting for this instance.
+   * @since 0.15
+   */
+  public SpanSelection getSpanSelection() {
+    return spanSelection;
+  }
+
+  // This method avoids getting the current span when the feature is disabled, for efficiency.
+  @Override
+  public void enhanceLogEntry(LogEntry.Builder builder) {
+    switch (spanSelection) {
+      case NO_SPANS:
+        return;
+      case SAMPLED_SPANS:
+        SpanContext span = getCurrentSpanContext();
+        if (span.getTraceOptions().isSampled()) {
+          addTracingData(tracePrefix, span, builder);
+        }
+        return;
+      case ALL_SPANS:
+        addTracingData(tracePrefix, getCurrentSpanContext(), builder);
+        return;
+    }
+    throw new AssertionError("Unknown spanSelection: " + spanSelection);
+  }
+
+  private static SpanContext getCurrentSpanContext() {
+    Span span = ContextUtils.CONTEXT_SPAN_KEY.get();
+    return span == null ? SpanContext.INVALID : span.getContext();
+  }
+
+  private static void addTracingData(
+      String tracePrefix, SpanContext span, LogEntry.Builder builder) {
+    builder.setTrace(formatTraceId(tracePrefix, span.getTraceId()));
+    builder.setSpanId(span.getSpanId().toLowerBase16());
+
+    // TODO(sebright): Find the correct way to add the sampling decision.
+    builder.addLabel(SAMPLED_LABEL_KEY, Boolean.toString(span.getTraceOptions().isSampled()));
+  }
+
+  private static String formatTraceId(String tracePrefix, TraceId traceId) {
+    return tracePrefix + traceId.toLowerBase16();
+  }
+}
diff --git a/contrib/log_correlation/stackdriver/src/test/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancerTest.java b/contrib/log_correlation/stackdriver/src/test/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancerTest.java
new file mode 100644
index 0000000..c116f09
--- /dev/null
+++ b/contrib/log_correlation/stackdriver/src/test/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancerTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2018, 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.logcorrelation.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.logging.LogEntry;
+import com.google.cloud.logging.LoggingEnhancer;
+import com.google.common.base.Charsets;
+import com.google.common.io.CharSource;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.SpanSelection;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.Tracing;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.logging.LogManager;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link OpenCensusTraceLoggingEnhancer}. */
+// TODO(sebright): Find a way to test that OpenCensusTraceLoggingEnhancer is called from Stackdriver
+// logging. See
+// https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/TESTING.md#testing-code-that-uses-logging.
+@RunWith(JUnit4.class)
+public class OpenCensusTraceLoggingEnhancerTest {
+  private static final String GOOGLE_CLOUD_PROJECT = "GOOGLE_CLOUD_PROJECT";
+  private static final Tracestate EMPTY_TRACESTATE = Tracestate.builder().build();
+
+  private static final Tracer tracer = Tracing.getTracer();
+
+  @Test
+  public void enhanceLogEntry_DoNotAddSampledSpanToLogEntryWithNoSpans() {
+    LogEntry logEntry =
+        getEnhancedLogEntry(
+            new OpenCensusTraceLoggingEnhancer("my-test-project-1", SpanSelection.NO_SPANS),
+            new TestSpan(
+                SpanContext.create(
+                    TraceId.fromLowerBase16("3da31be987098abb08c71c7700d2680e"),
+                    SpanId.fromLowerBase16("51b109f15e0d3881"),
+                    TraceOptions.builder().setIsSampled(true).build(),
+                    EMPTY_TRACESTATE)));
+    assertContainsNoTracingData(logEntry);
+  }
+
+  @Test
+  public void enhanceLogEntry_AddSampledSpanToLogEntryWithSampledSpans() {
+    LogEntry logEntry =
+        getEnhancedLogEntry(
+            new OpenCensusTraceLoggingEnhancer("my-test-project-2", SpanSelection.SAMPLED_SPANS),
+            new TestSpan(
+                SpanContext.create(
+                    TraceId.fromLowerBase16("4c9874d0b41224cce77ff74ee10f5ee6"),
+                    SpanId.fromLowerBase16("592ae363e92cb3dd"),
+                    TraceOptions.builder().setIsSampled(true).build(),
+                    EMPTY_TRACESTATE)));
+    assertThat(logEntry.getLabels()).containsEntry("opencensusTraceSampled", "true");
+    assertThat(logEntry.getTrace())
+        .isEqualTo("projects/my-test-project-2/traces/4c9874d0b41224cce77ff74ee10f5ee6");
+    assertThat(logEntry.getSpanId()).isEqualTo("592ae363e92cb3dd");
+  }
+
+  @Test
+  public void enhanceLogEntry_AddSampledSpanToLogEntryWithAllSpans() {
+    LogEntry logEntry =
+        getEnhancedLogEntry(
+            new OpenCensusTraceLoggingEnhancer("my-test-project-3", SpanSelection.ALL_SPANS),
+            new TestSpan(
+                SpanContext.create(
+                    TraceId.fromLowerBase16("4c6af40c499951eb7de2777ba1e4fefa"),
+                    SpanId.fromLowerBase16("de52e84d13dd232d"),
+                    TraceOptions.builder().setIsSampled(true).build(),
+                    EMPTY_TRACESTATE)));
+    assertThat(logEntry.getLabels()).containsEntry("opencensusTraceSampled", "true");
+    assertThat(logEntry.getTrace())
+        .isEqualTo("projects/my-test-project-3/traces/4c6af40c499951eb7de2777ba1e4fefa");
+    assertThat(logEntry.getSpanId()).isEqualTo("de52e84d13dd232d");
+  }
+
+  @Test
+  public void enhanceLogEntry_DoNotAddNonSampledSpanToLogEntryWithNoSpans() {
+    LogEntry logEntry =
+        getEnhancedLogEntry(
+            new OpenCensusTraceLoggingEnhancer("my-test-project-4", SpanSelection.NO_SPANS),
+            new TestSpan(
+                SpanContext.create(
+                    TraceId.fromLowerBase16("88ab22b18b97369df065ca830e41cf6a"),
+                    SpanId.fromLowerBase16("8987d372039021fd"),
+                    TraceOptions.builder().setIsSampled(false).build(),
+                    EMPTY_TRACESTATE)));
+    assertContainsNoTracingData(logEntry);
+  }
+
+  @Test
+  public void enhanceLogEntry_DoNotAddNonSampledSpanToLogEntryWithSampledSpans() {
+    LogEntry logEntry =
+        getEnhancedLogEntry(
+            new OpenCensusTraceLoggingEnhancer("my-test-project-5", SpanSelection.SAMPLED_SPANS),
+            new TestSpan(
+                SpanContext.create(
+                    TraceId.fromLowerBase16("7f4703d9bb02f4f2e67fb840103cdd34"),
+                    SpanId.fromLowerBase16("2d7d95a555557434"),
+                    TraceOptions.builder().setIsSampled(false).build(),
+                    EMPTY_TRACESTATE)));
+    assertContainsNoTracingData(logEntry);
+  }
+
+  @Test
+  public void enhanceLogEntry_AddNonSampledSpanToLogEntryWithAllSpans() {
+    LogEntry logEntry =
+        getEnhancedLogEntry(
+            new OpenCensusTraceLoggingEnhancer("my-test-project-6", SpanSelection.ALL_SPANS),
+            new TestSpan(
+                SpanContext.create(
+                    TraceId.fromLowerBase16("72c905c76f99e99974afd84dc053a480"),
+                    SpanId.fromLowerBase16("731e102335b7a5a0"),
+                    TraceOptions.builder().setIsSampled(false).build(),
+                    EMPTY_TRACESTATE)));
+    assertThat(logEntry.getLabels()).containsEntry("opencensusTraceSampled", "false");
+    assertThat(logEntry.getTrace())
+        .isEqualTo("projects/my-test-project-6/traces/72c905c76f99e99974afd84dc053a480");
+    assertThat(logEntry.getSpanId()).isEqualTo("731e102335b7a5a0");
+  }
+
+  @Test
+  public void enhanceLogEntry_AddBlankSpanToLogEntryWithAllSpans() {
+    LogEntry logEntry =
+        getEnhancedLogEntry(
+            new OpenCensusTraceLoggingEnhancer("my-test-project-7", SpanSelection.ALL_SPANS),
+            BlankSpan.INSTANCE);
+    assertThat(logEntry.getLabels().get("opencensusTraceSampled")).isEqualTo("false");
+    assertThat(logEntry.getTrace())
+        .isEqualTo("projects/my-test-project-7/traces/00000000000000000000000000000000");
+    assertThat(logEntry.getSpanId()).isEqualTo("0000000000000000");
+  }
+
+  @Test
+  public void enhanceLogEntry_ConvertNullProjectIdToEmptyString() {
+    LogEntry logEntry =
+        getEnhancedLogEntry(
+            new OpenCensusTraceLoggingEnhancer(null, SpanSelection.ALL_SPANS),
+            new TestSpan(
+                SpanContext.create(
+                    TraceId.fromLowerBase16("bfb4248a24325a905873a1d43001d9a0"),
+                    SpanId.fromLowerBase16("6f23f9afd448e272"),
+                    TraceOptions.builder().setIsSampled(true).build(),
+                    EMPTY_TRACESTATE)));
+    assertThat(logEntry.getTrace()).isEqualTo("projects//traces/bfb4248a24325a905873a1d43001d9a0");
+  }
+
+  private static LogEntry getEnhancedLogEntry(LoggingEnhancer loggingEnhancer, Span span) {
+    Scope scope = tracer.withSpan(span);
+    try {
+      LogEntry.Builder builder = LogEntry.newBuilder(null);
+      loggingEnhancer.enhanceLogEntry(builder);
+      return builder.build();
+    } finally {
+      scope.close();
+    }
+  }
+
+  private static void assertContainsNoTracingData(LogEntry logEntry) {
+    assertThat(logEntry.getLabels()).doesNotContainKey("opencensusTraceSampled");
+    assertThat(logEntry.getTrace()).isNull();
+    assertThat(logEntry.getSpanId()).isNull();
+  }
+
+  @Test
+  public void spanSelectionDefaultIsAllSpans() {
+    assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+        .isEqualTo(SpanSelection.ALL_SPANS);
+  }
+
+  @Test
+  @SuppressWarnings("TruthConstantAsserts")
+  public void projectIdPropertyName() {
+    assertThat(OpenCensusTraceLoggingEnhancer.PROJECT_ID_PROPERTY_NAME)
+        .isEqualTo(OpenCensusTraceLoggingEnhancer.class.getName() + ".projectId");
+  }
+
+  @Test
+  @SuppressWarnings("TruthConstantAsserts")
+  public void spanSelectionPropertyName() {
+    assertThat(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME)
+        .isEqualTo(OpenCensusTraceLoggingEnhancer.class.getName() + ".spanSelection");
+  }
+
+  @Test
+  public void setProjectIdWithGoogleCloudJava() {
+    try {
+      System.setProperty(GOOGLE_CLOUD_PROJECT, "my-project-id");
+      assertThat(new OpenCensusTraceLoggingEnhancer().getProjectId()).isEqualTo("my-project-id");
+    } finally {
+      System.clearProperty(GOOGLE_CLOUD_PROJECT);
+    }
+  }
+
+  @Test
+  public void overrideProjectIdWithSystemProperty() {
+    try {
+      System.setProperty(
+          OpenCensusTraceLoggingEnhancer.PROJECT_ID_PROPERTY_NAME, "project ID override");
+      try {
+        System.setProperty(GOOGLE_CLOUD_PROJECT, "GOOGLE_CLOUD_PROJECT project ID");
+        assertThat(new OpenCensusTraceLoggingEnhancer().getProjectId())
+            .isEqualTo("project ID override");
+      } finally {
+        System.clearProperty(GOOGLE_CLOUD_PROJECT);
+      }
+    } finally {
+      System.clearProperty(OpenCensusTraceLoggingEnhancer.PROJECT_ID_PROPERTY_NAME);
+    }
+  }
+
+  @Test
+  public void setSpanSelectionWithSystemProperty() {
+    try {
+      System.setProperty(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME, "NO_SPANS");
+      assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+          .isEqualTo(SpanSelection.NO_SPANS);
+    } finally {
+      System.clearProperty(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME);
+    }
+  }
+
+  @Test
+  public void overrideProjectIdWithLoggingProperty() throws IOException {
+    try {
+      LogManager.getLogManager()
+          .readConfiguration(
+              stringToInputStream(
+                  OpenCensusTraceLoggingEnhancer.PROJECT_ID_PROPERTY_NAME + "=PROJECT_OVERRIDE"));
+      try {
+        System.setProperty(GOOGLE_CLOUD_PROJECT, "GOOGLE_CLOUD_PROJECT project ID");
+        assertThat(new OpenCensusTraceLoggingEnhancer().getProjectId())
+            .isEqualTo("PROJECT_OVERRIDE");
+      } finally {
+        System.clearProperty(GOOGLE_CLOUD_PROJECT);
+      }
+    } finally {
+      LogManager.getLogManager().reset();
+    }
+  }
+
+  @Test
+  public void setSpanSelectionWithLoggingProperty() throws IOException {
+    try {
+      LogManager.getLogManager()
+          .readConfiguration(
+              stringToInputStream(
+                  OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME + "=SAMPLED_SPANS"));
+      assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+          .isEqualTo(SpanSelection.SAMPLED_SPANS);
+    } finally {
+      LogManager.getLogManager().reset();
+    }
+  }
+
+  @Test
+  public void loggingPropertyTakesPrecedenceOverSystemProperty() throws IOException {
+    try {
+      LogManager.getLogManager()
+          .readConfiguration(
+              stringToInputStream(
+                  OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME + "=NO_SPANS"));
+      try {
+        System.setProperty(
+            OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME, "SAMPLED_SPANS");
+        assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+            .isEqualTo(SpanSelection.NO_SPANS);
+      } finally {
+        System.clearProperty(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME);
+      }
+    } finally {
+      LogManager.getLogManager().reset();
+    }
+  }
+
+  private static InputStream stringToInputStream(String contents) throws IOException {
+    return CharSource.wrap(contents).asByteSource(Charsets.UTF_8).openBufferedStream();
+  }
+
+  @Test
+  public void useDefaultValueForInvalidSpanSelection() {
+    try {
+      System.setProperty(
+          OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME, "INVALID_SPAN_SELECTION");
+      assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+          .isEqualTo(SpanSelection.ALL_SPANS);
+    } finally {
+      System.clearProperty(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME);
+    }
+  }
+
+  private static final class TestSpan extends Span {
+    TestSpan(SpanContext context) {
+      super(context, EnumSet.of(Options.RECORD_EVENTS));
+    }
+
+    @Override
+    public void end(EndSpanOptions options) {}
+
+    @Override
+    public void addLink(Link link) {}
+
+    @Override
+    public void addAnnotation(Annotation annotation) {}
+
+    @Override
+    public void addAnnotation(String description, Map<String, AttributeValue> attributes) {}
+  }
+}
diff --git a/contrib/monitored_resource_util/README.md b/contrib/monitored_resource_util/README.md
new file mode 100644
index 0000000..9d3c754
--- /dev/null
+++ b/contrib/monitored_resource_util/README.md
@@ -0,0 +1,34 @@
+# OpenCensus Monitored Resources Util
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Monitored Resource Util for Java* is a collection of utilities for auto detecting
+monitored resource when exporting stats, based on the environment where the application is running.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-monitored-resource-util</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-monitored-resource-util:0.16.1'
+```
+
+[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-monitoredresource-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-monitoredresource-util
diff --git a/contrib/monitored_resource_util/build.gradle b/contrib/monitored_resource_util/build.gradle
new file mode 100644
index 0000000..1e25c7c
--- /dev/null
+++ b/contrib/monitored_resource_util/build.gradle
@@ -0,0 +1,15 @@
+description = 'OpenCensus Monitored Resource Util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+dependencies {
+    compileOnly libraries.auto_value
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtils.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtils.java
new file mode 100644
index 0000000..03b0bd4
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtils.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2018, 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.monitoredresource.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.GuardedBy;
+
+/** Util methods for getting and parsing AWS instance identity document. */
+final class AwsIdentityDocUtils {
+
+  private static final Object monitor = new Object();
+  private static final int AWS_IDENTITY_DOC_BUF_SIZE = 0x800; // 2K chars (4K bytes)
+  private static final String AWS_IDENTITY_DOC_LINE_BREAK_SPLITTER = "\n";
+  private static final String AWS_IDENTITY_DOC_COLON_SPLITTER = ":";
+
+  private static final URI AWS_INSTANCE_IDENTITY_DOCUMENT_URI =
+      URI.create("http://169.254.169.254/latest/dynamic/instance-identity/document");
+
+  @GuardedBy("monitor")
+  @javax.annotation.Nullable
+  private static Map<String, String> awsEnvVarMap = null;
+
+  // Detects if the application is running on EC2 by making a connection to AWS instance
+  // identity document URI. If connection is successful, application should be on an EC2 instance.
+  private static volatile boolean isRunningOnAwsEc2 = false;
+
+  static {
+    initializeAwsIdentityDocument();
+  }
+
+  static boolean isRunningOnAwsEc2() {
+    return isRunningOnAwsEc2;
+  }
+
+  // Tries to establish an HTTP connection to AWS instance identity document url. If the application
+  // is running on an EC2 instance, we should be able to get back a valid JSON document. Parses that
+  // document and stores the identity properties in a local map.
+  // This method should only be called once.
+  private static void initializeAwsIdentityDocument() {
+    InputStream stream = null;
+    try {
+      stream = openStream(AWS_INSTANCE_IDENTITY_DOCUMENT_URI);
+      String awsIdentityDocument = slurp(new InputStreamReader(stream, Charset.forName("UTF-8")));
+      synchronized (monitor) {
+        awsEnvVarMap = parseAwsIdentityDocument(awsIdentityDocument);
+      }
+      isRunningOnAwsEc2 = true;
+    } catch (IOException e) {
+      // Cannot connect to http://169.254.169.254/latest/dynamic/instance-identity/document.
+      // Not on an AWS EC2 instance.
+    } finally {
+      if (stream != null) {
+        try {
+          stream.close();
+        } catch (IOException e) {
+          // Do nothing.
+        }
+      }
+    }
+  }
+
+  /** quick http client that allows no-dependency try at getting instance data. */
+  private static InputStream openStream(URI uri) throws IOException {
+    HttpURLConnection connection = HttpURLConnection.class.cast(uri.toURL().openConnection());
+    connection.setConnectTimeout(1000 * 2);
+    connection.setReadTimeout(1000 * 2);
+    connection.setAllowUserInteraction(false);
+    connection.setInstanceFollowRedirects(false);
+    return connection.getInputStream();
+  }
+
+  /** returns the {@code reader} as a string without closing it. */
+  private static String slurp(Reader reader) throws IOException {
+    StringBuilder to = new StringBuilder();
+    CharBuffer buf = CharBuffer.allocate(AWS_IDENTITY_DOC_BUF_SIZE);
+    while (reader.read(buf) != -1) {
+      buf.flip();
+      to.append(buf);
+      buf.clear();
+    }
+    return to.toString();
+  }
+
+  // AWS Instance Identity Document is a JSON file.
+  // See docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html.
+  static Map<String, String> parseAwsIdentityDocument(String awsIdentityDocument) {
+    Map<String, String> map = new HashMap<String, String>();
+    @SuppressWarnings("StringSplitter")
+    String[] lines = awsIdentityDocument.split(AWS_IDENTITY_DOC_LINE_BREAK_SPLITTER, -1);
+    for (String line : lines) {
+      @SuppressWarnings("StringSplitter")
+      String[] keyValuePair = line.split(AWS_IDENTITY_DOC_COLON_SPLITTER, -1);
+      if (keyValuePair.length != 2) {
+        continue;
+      }
+      String key = keyValuePair[0].replaceAll("[\" ]", "");
+      String value = keyValuePair[1].replaceAll("[\" ,]", "");
+      map.put(key, value);
+    }
+    return map;
+  }
+
+  @javax.annotation.Nullable
+  static String getValueFromAwsIdentityDocument(String key) {
+    synchronized (monitor) {
+      if (awsEnvVarMap == null) {
+        return null;
+      }
+      return awsEnvVarMap.get(key);
+    }
+  }
+
+  private AwsIdentityDocUtils() {}
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/GcpMetadataConfig.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/GcpMetadataConfig.java
new file mode 100644
index 0000000..c09d1c6
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/GcpMetadataConfig.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018, 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.monitoredresource.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.Charset;
+import javax.annotation.Nullable;
+
+/**
+ * Retrieves Google Cloud project-id and a limited set of instance attributes from Metadata server.
+ *
+ * @see <a href="https://cloud.google.com/compute/docs/storing-retrieving-metadata">
+ *     https://cloud.google.com/compute/docs/storing-retrieving-metadata</a>
+ */
+final class GcpMetadataConfig {
+
+  private static final String METADATA_URL = "http://metadata/computeMetadata/v1/";
+
+  private GcpMetadataConfig() {}
+
+  @Nullable
+  static String getProjectId() {
+    return getAttribute("project/project-id");
+  }
+
+  @Nullable
+  static String getZone() {
+    String zoneId = getAttribute("instance/zone");
+    if (zoneId == null) {
+      return null;
+    }
+    if (zoneId.contains("/")) {
+      return zoneId.substring(zoneId.lastIndexOf('/') + 1);
+    }
+    return zoneId;
+  }
+
+  @Nullable
+  static String getInstanceId() {
+    return getAttribute("instance/id");
+  }
+
+  @Nullable
+  static String getClusterName() {
+    return getAttribute("instance/attributes/cluster-name");
+  }
+
+  @Nullable
+  private static String getAttribute(String attributeName) {
+    try {
+      URL url = new URL(METADATA_URL + attributeName);
+      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+      connection.setRequestProperty("Metadata-Flavor", "Google");
+      InputStream input = connection.getInputStream();
+      if (connection.getResponseCode() == 200) {
+        BufferedReader reader = null;
+        try {
+          reader = new BufferedReader(new InputStreamReader(input, Charset.forName("UTF-8")));
+          return reader.readLine();
+        } finally {
+          if (reader != null) {
+            reader.close();
+          }
+        }
+      }
+    } catch (IOException ignore) {
+      // ignore
+    }
+    return null;
+  }
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResource.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResource.java
new file mode 100644
index 0000000..c828906
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResource.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2018, 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.monitoredresource.util;
+
+import com.google.auto.value.AutoValue;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link MonitoredResource} represents an auto-detected monitored resource used by application for
+ * exporting stats. It has a {@code ResourceType} associated with a mapping from resource labels to
+ * values.
+ *
+ * @since 0.13
+ */
+@Immutable
+public abstract class MonitoredResource {
+
+  MonitoredResource() {}
+
+  /**
+   * Returns the {@link ResourceType} of this {@link MonitoredResource}.
+   *
+   * @return the {@code ResourceType}.
+   * @since 0.13
+   */
+  public abstract ResourceType getResourceType();
+
+  /*
+   * Returns the first of two given parameters that is not null, if either is, or otherwise
+   * throws a NullPointerException.
+   */
+  private static <T> T firstNonNull(@Nullable T first, @Nullable T second) {
+    if (first != null) {
+      return first;
+    }
+    if (second != null) {
+      return second;
+    }
+    throw new NullPointerException("Both parameters are null");
+  }
+
+  // TODO(songya): consider using a tagged union match() approach (that will introduce
+  // dependency on opencensus-api).
+
+  /**
+   * {@link MonitoredResource} for AWS EC2 instance.
+   *
+   * @since 0.13
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class AwsEc2InstanceMonitoredResource extends MonitoredResource {
+
+    private static final String AWS_ACCOUNT =
+        firstNonNull(AwsIdentityDocUtils.getValueFromAwsIdentityDocument("accountId"), "");
+    private static final String AWS_INSTANCE_ID =
+        firstNonNull(AwsIdentityDocUtils.getValueFromAwsIdentityDocument("instanceId"), "");
+    private static final String AWS_REGION =
+        firstNonNull(AwsIdentityDocUtils.getValueFromAwsIdentityDocument("region"), "");
+
+    @Override
+    public ResourceType getResourceType() {
+      return ResourceType.AWS_EC2_INSTANCE;
+    }
+
+    /**
+     * Returns the AWS account ID.
+     *
+     * @return the AWS account ID.
+     * @since 0.13
+     */
+    public abstract String getAccount();
+
+    /**
+     * Returns the AWS EC2 instance ID.
+     *
+     * @return the AWS EC2 instance ID.
+     * @since 0.13
+     */
+    public abstract String getInstanceId();
+
+    /**
+     * Returns the AWS region.
+     *
+     * @return the AWS region.
+     * @since 0.13
+     */
+    public abstract String getRegion();
+
+    /**
+     * Returns an {@link AwsEc2InstanceMonitoredResource}.
+     *
+     * @param account the AWS account ID.
+     * @param instanceId the AWS EC2 instance ID.
+     * @param region the AWS region.
+     * @return an {@code AwsEc2InstanceMonitoredResource}.
+     * @since 0.15
+     */
+    public static AwsEc2InstanceMonitoredResource create(
+        String account, String instanceId, String region) {
+      return new AutoValue_MonitoredResource_AwsEc2InstanceMonitoredResource(
+          account, instanceId, region);
+    }
+
+    static AwsEc2InstanceMonitoredResource create() {
+      return create(AWS_ACCOUNT, AWS_INSTANCE_ID, AWS_REGION);
+    }
+  }
+
+  /**
+   * {@link MonitoredResource} for GCP GCE instance.
+   *
+   * @since 0.13
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class GcpGceInstanceMonitoredResource extends MonitoredResource {
+
+    private static final String GCP_ACCOUNT_ID = firstNonNull(GcpMetadataConfig.getProjectId(), "");
+    private static final String GCP_INSTANCE_ID =
+        firstNonNull(GcpMetadataConfig.getInstanceId(), "");
+    private static final String GCP_ZONE = firstNonNull(GcpMetadataConfig.getZone(), "");
+
+    @Override
+    public ResourceType getResourceType() {
+      return ResourceType.GCP_GCE_INSTANCE;
+    }
+
+    /**
+     * Returns the GCP account number for the instance.
+     *
+     * @return the GCP account number for the instance.
+     * @since 0.13
+     */
+    public abstract String getAccount();
+
+    /**
+     * Returns the GCP GCE instance ID.
+     *
+     * @return the GCP GCE instance ID.
+     * @since 0.13
+     */
+    public abstract String getInstanceId();
+
+    /**
+     * Returns the GCP zone.
+     *
+     * @return the GCP zone.
+     * @since 0.13
+     */
+    public abstract String getZone();
+
+    /**
+     * Returns a {@link GcpGceInstanceMonitoredResource}.
+     *
+     * @param account the GCP account number.
+     * @param instanceId the GCP GCE instance ID.
+     * @param zone the GCP zone.
+     * @return a {@code GcpGceInstanceMonitoredResource}.
+     * @since 0.15
+     */
+    public static GcpGceInstanceMonitoredResource create(
+        String account, String instanceId, String zone) {
+      return new AutoValue_MonitoredResource_GcpGceInstanceMonitoredResource(
+          account, instanceId, zone);
+    }
+
+    static GcpGceInstanceMonitoredResource create() {
+      return create(GCP_ACCOUNT_ID, GCP_INSTANCE_ID, GCP_ZONE);
+    }
+  }
+
+  /**
+   * {@link MonitoredResource} for GCP GKE container.
+   *
+   * @since 0.13
+   */
+  @Immutable
+  @AutoValue
+  public abstract static class GcpGkeContainerMonitoredResource extends MonitoredResource {
+
+    private static final String GCP_ACCOUNT_ID = firstNonNull(GcpMetadataConfig.getProjectId(), "");
+    private static final String GCP_CLUSTER_NAME =
+        firstNonNull(GcpMetadataConfig.getClusterName(), "");
+    private static final String GCP_CONTAINER_NAME =
+        firstNonNull(System.getenv("CONTAINER_NAME"), "");
+    private static final String GCP_NAMESPACE_ID = firstNonNull(System.getenv("NAMESPACE"), "");
+    private static final String GCP_INSTANCE_ID =
+        firstNonNull(GcpMetadataConfig.getInstanceId(), "");
+    private static final String GCP_POD_ID = firstNonNull(System.getenv("HOSTNAME"), "");
+    private static final String GCP_ZONE = firstNonNull(GcpMetadataConfig.getZone(), "");
+
+    @Override
+    public ResourceType getResourceType() {
+      return ResourceType.GCP_GKE_CONTAINER;
+    }
+
+    /**
+     * Returns the GCP account number for the instance.
+     *
+     * @return the GCP account number for the instance.
+     * @since 0.13
+     */
+    public abstract String getAccount();
+
+    /**
+     * Returns the GCP GKE cluster name.
+     *
+     * @return the GCP GKE cluster name.
+     * @since 0.13
+     */
+    public abstract String getClusterName();
+
+    /**
+     * Returns the GCP GKE container name.
+     *
+     * @return the GCP GKE container name.
+     * @since 0.13
+     */
+    public abstract String getContainerName();
+
+    /**
+     * Returns the GCP GKE namespace ID.
+     *
+     * @return the GCP GKE namespace ID.
+     * @since 0.13
+     */
+    public abstract String getNamespaceId();
+
+    /**
+     * Returns the GCP GKE instance ID.
+     *
+     * @return the GCP GKE instance ID.
+     * @since 0.13
+     */
+    public abstract String getInstanceId();
+
+    /**
+     * Returns the GCP GKE Pod ID.
+     *
+     * @return the GCP GKE Pod ID.
+     * @since 0.13
+     */
+    public abstract String getPodId();
+
+    /**
+     * Returns the GCP zone.
+     *
+     * @return the GCP zone.
+     * @since 0.13
+     */
+    public abstract String getZone();
+
+    /**
+     * Returns a {@link GcpGkeContainerMonitoredResource}.
+     *
+     * @param account the GCP account number.
+     * @param clusterName the GCP GKE cluster name.
+     * @param containerName the GCP GKE container name.
+     * @param namespaceId the GCP GKE namespace ID.
+     * @param instanceId the GCP GKE instance ID.
+     * @param podId the GCP GKE Pod ID.
+     * @param zone the GCP zone.
+     * @return a {@code GcpGkeContainerMonitoredResource}.
+     * @since 0.15
+     */
+    public static GcpGkeContainerMonitoredResource create(
+        String account,
+        String clusterName,
+        String containerName,
+        String namespaceId,
+        String instanceId,
+        String podId,
+        String zone) {
+      return new AutoValue_MonitoredResource_GcpGkeContainerMonitoredResource(
+          account, clusterName, containerName, namespaceId, instanceId, podId, zone);
+    }
+
+    static GcpGkeContainerMonitoredResource create() {
+      return create(
+          GCP_ACCOUNT_ID,
+          GCP_CLUSTER_NAME,
+          GCP_CONTAINER_NAME,
+          GCP_NAMESPACE_ID,
+          GCP_INSTANCE_ID,
+          GCP_POD_ID,
+          GCP_ZONE);
+    }
+  }
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtils.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtils.java
new file mode 100644
index 0000000..8ff0ff9
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.monitoredresource.util;
+
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import javax.annotation.Nullable;
+
+/**
+ * Utilities for for auto detecting monitored resource based on the environment where the
+ * application is running.
+ *
+ * @since 0.13
+ */
+public final class MonitoredResourceUtils {
+
+  /**
+   * Returns a self-configured monitored resource, or {@code null} if the application is not running
+   * on a supported environment.
+   *
+   * @return a {@code MonitoredResource}.
+   * @since 0.13
+   */
+  @Nullable
+  public static MonitoredResource getDefaultResource() {
+    if (System.getenv("KUBERNETES_SERVICE_HOST") != null) {
+      return GcpGkeContainerMonitoredResource.create();
+    }
+    if (GcpMetadataConfig.getInstanceId() != null) {
+      return GcpGceInstanceMonitoredResource.create();
+    }
+    if (AwsIdentityDocUtils.isRunningOnAwsEc2()) {
+      return AwsEc2InstanceMonitoredResource.create();
+    }
+    return null;
+  }
+
+  private MonitoredResourceUtils() {}
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/ResourceType.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/ResourceType.java
new file mode 100644
index 0000000..f281667
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/ResourceType.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018, 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.monitoredresource.util;
+
+/**
+ * {@link ResourceType} represents the type of supported monitored resources that can be
+ * automatically detected by OpenCensus.
+ *
+ * @since 0.13
+ */
+public enum ResourceType {
+
+  /**
+   * Resource for GCP GKE container.
+   *
+   * @since 0.13
+   */
+  GCP_GKE_CONTAINER,
+
+  /**
+   * Resource for GCP GCE instance.
+   *
+   * @since 0.13
+   */
+  GCP_GCE_INSTANCE,
+
+  /**
+   * Resource for AWS EC2 instance.
+   *
+   * @since 0.13
+   */
+  AWS_EC2_INSTANCE
+}
diff --git a/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtilsTest.java b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtilsTest.java
new file mode 100644
index 0000000..77d9849
--- /dev/null
+++ b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtilsTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018, 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.monitoredresource.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AwsIdentityDocUtils}. */
+@RunWith(JUnit4.class)
+public class AwsIdentityDocUtilsTest {
+
+  private static final String SAMPLE_AWS_IDENTITY_DOCUMENT =
+      "{\n"
+          + "    \"devpayProductCodes\" : null,\n"
+          + "    \"marketplaceProductCodes\" : [ \"1abc2defghijklm3nopqrs4tu\" ], \n"
+          + "    \"availabilityZone\" : \"us-west-2b\",\n"
+          + "    \"privateIp\" : \"10.158.112.84\",\n"
+          + "    \"version\" : \"2017-09-30\",\n"
+          + "    \"instanceId\" : \"i-1234567890abcdef0\",\n"
+          + "    \"billingProducts\" : null,\n"
+          + "    \"instanceType\" : \"t2.micro\",\n"
+          + "    \"accountId\" : \"123456789012\",\n"
+          + "    \"imageId\" : \"ami-5fb8c835\",\n"
+          + "    \"pendingTime\" : \"2016-11-19T16:32:11Z\",\n"
+          + "    \"architecture\" : \"x86_64\",\n"
+          + "    \"kernelId\" : null,\n"
+          + "    \"ramdiskId\" : null,\n"
+          + "    \"region\" : \"us-west-2\"\n"
+          + "}";
+
+  @Test
+  public void testParseAwsIdentityDocument() {
+    Map<String, String> envVarMap =
+        AwsIdentityDocUtils.parseAwsIdentityDocument(SAMPLE_AWS_IDENTITY_DOCUMENT);
+    assertThat(envVarMap).containsEntry("instanceId", "i-1234567890abcdef0");
+    assertThat(envVarMap).containsEntry("accountId", "123456789012");
+    assertThat(envVarMap).containsEntry("region", "us-west-2");
+  }
+}
diff --git a/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceTest.java b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceTest.java
new file mode 100644
index 0000000..0defcbd
--- /dev/null
+++ b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018, 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.monitoredresource.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MonitoredResource}. */
+@RunWith(JUnit4.class)
+public class MonitoredResourceTest {
+
+  private static final String AWS_ACCOUNT = "aws-account";
+  private static final String AWS_INSTANCE = "instance";
+  private static final String AWS_REGION = "us-west-2";
+  private static final String GCP_PROJECT = "gcp-project";
+  private static final String GCP_INSTANCE = "instance";
+  private static final String GCP_ZONE = "us-east1";
+  private static final String GCP_GKE_NAMESPACE = "namespace";
+  private static final String GCP_GKE_POD_ID = "pod-id";
+  private static final String GCP_GKE_CONTAINER_NAME = "container";
+  private static final String GCP_GKE_CLUSTER_NAME = "cluster";
+
+  @Test
+  public void testAwsEc2InstanceMonitoredResource() {
+    AwsEc2InstanceMonitoredResource resource =
+        AwsEc2InstanceMonitoredResource.create(AWS_ACCOUNT, AWS_INSTANCE, AWS_REGION);
+    assertThat(resource.getResourceType()).isEqualTo(ResourceType.AWS_EC2_INSTANCE);
+    assertThat(resource.getAccount()).isEqualTo(AWS_ACCOUNT);
+    assertThat(resource.getInstanceId()).isEqualTo(AWS_INSTANCE);
+    assertThat(resource.getRegion()).isEqualTo(AWS_REGION);
+  }
+
+  @Test
+  public void testGcpGceInstanceMonitoredResource() {
+    GcpGceInstanceMonitoredResource resource =
+        GcpGceInstanceMonitoredResource.create(GCP_PROJECT, GCP_INSTANCE, GCP_ZONE);
+    assertThat(resource.getResourceType()).isEqualTo(ResourceType.GCP_GCE_INSTANCE);
+    assertThat(resource.getAccount()).isEqualTo(GCP_PROJECT);
+    assertThat(resource.getInstanceId()).isEqualTo(GCP_INSTANCE);
+    assertThat(resource.getZone()).isEqualTo(GCP_ZONE);
+  }
+
+  @Test
+  public void testGcpGkeContainerMonitoredResource() {
+    GcpGkeContainerMonitoredResource resource =
+        GcpGkeContainerMonitoredResource.create(
+            GCP_PROJECT,
+            GCP_GKE_CLUSTER_NAME,
+            GCP_GKE_CONTAINER_NAME,
+            GCP_GKE_NAMESPACE,
+            GCP_INSTANCE,
+            GCP_GKE_POD_ID,
+            GCP_ZONE);
+    assertThat(resource.getResourceType()).isEqualTo(ResourceType.GCP_GKE_CONTAINER);
+    assertThat(resource.getAccount()).isEqualTo(GCP_PROJECT);
+    assertThat(resource.getClusterName()).isEqualTo(GCP_GKE_CLUSTER_NAME);
+    assertThat(resource.getContainerName()).isEqualTo(GCP_GKE_CONTAINER_NAME);
+    assertThat(resource.getNamespaceId()).isEqualTo(GCP_GKE_NAMESPACE);
+    assertThat(resource.getInstanceId()).isEqualTo(GCP_INSTANCE);
+    assertThat(resource.getPodId()).isEqualTo(GCP_GKE_POD_ID);
+    assertThat(resource.getZone()).isEqualTo(GCP_ZONE);
+  }
+}
diff --git a/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtilsTest.java b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtilsTest.java
new file mode 100644
index 0000000..01927a2
--- /dev/null
+++ b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtilsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018, 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.monitoredresource.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MonitoredResourceUtils}. */
+@RunWith(JUnit4.class)
+public class MonitoredResourceUtilsTest {
+
+  @Test
+  public void testGetDefaultResource() {
+    MonitoredResource resource = MonitoredResourceUtils.getDefaultResource();
+    if (System.getenv("KUBERNETES_SERVICE_HOST") != null) {
+      assertThat(resource.getResourceType()).isEqualTo(ResourceType.GCP_GKE_CONTAINER);
+    } else if (GcpMetadataConfig.getInstanceId() != null) {
+      assertThat(resource.getResourceType()).isEqualTo(ResourceType.GCP_GCE_INSTANCE);
+    } else if (AwsIdentityDocUtils.isRunningOnAwsEc2()) {
+      assertThat(resource.getResourceType()).isEqualTo(ResourceType.AWS_EC2_INSTANCE);
+    } else {
+      assertThat(resource).isNull();
+    }
+  }
+}
diff --git a/contrib/spring/README.md b/contrib/spring/README.md
new file mode 100644
index 0000000..8c74029
--- /dev/null
+++ b/contrib/spring/README.md
@@ -0,0 +1,160 @@
+# spring
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+Provides annotation support for projects that use Spring.  
+
+## Quickstart
+
+### Add the dependencies to your project.
+
+Replace `SPRING_VERSION` with the version of spring you're using.
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <!-- census -->
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-spring</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+  
+  <!-- spring aspects -->
+  <dependency>
+    <groupId>org.springframework</groupId>
+    <artifactId>spring-aspects</artifactId>
+    <version>SPRING_VERSION</version>
+    <scope>runtime</scope>
+  </dependency>
+  
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-contrib-spring:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+runtime 'org.springframework:spring-aspects:SPRING_VERSION'
+```
+
+### Features
+
+#### Traced Annotation
+
+The `opencensus-contrib-spring` package provides support for a `@Traced` annotation 
+that can be applied to methods.  When applied, the method will be wrapped in a 
+Span, [https://github.com/census-instrumentation/opencensus-specs/blob/master/trace/Span.md](https://github.com/census-instrumentation/opencensus-specs/blob/master/trace/Span.md)
+
+If the method throws an exception, the `Span` will be marked with a status of `Status.UNKNOWN`
+and the stack trace will be added to the span as an annotation.
+
+To enable the `@Traced` annotation, include the `CensusSpringAspect` bean.
+
+```xml
+  <!-- traces explicit calls to Traced -->
+  <bean id="censusAspect" class="io.opencensus.contrib.spring.aop.CensusSpringAspect">
+    <constructor-arg ref="tracer"/>
+  </bean>
+```
+
+#### Database Support
+
+The `opencensus-contrib-spring` package also includes support for tracing database
+calls.  When database support is included, all calls to `java.sql.PreparedStatement.execute*`
+will be wrapped in a Span in the same way that `@Traced` wraps methods.
+
+To enable database support, include the `CensusSpringSqlAspect` bean.
+
+```xml
+  <!-- traces all SQL calls -->
+  <bean id="censusSQLAspect" class="io.opencensus.contrib.spring.aop.CensusSpringSqlAspect">
+    <constructor-arg ref="tracer"/>
+  </bean>
+```
+
+#### Complete Spring XML configuration
+
+The following contains a complete spring xml file to configure `opencensus-contrib-spring` 
+with support for both `@Traced` and database connection tracing.
+
+**Note:** This example does not include the configuration of any exporters. That will 
+need to be done separately.
+
+**TBD:*** Include examples of spring with exporters.
+
+```xml
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:aop="http://www.springframework.org/schema/aop"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
+
+  <aop:aspectj-autoproxy/>
+
+  <!-- traces explicit calls to Traced -->
+  <bean id="censusAspect" class="io.opencensus.contrib.spring.aop.CensusSpringAspect">
+    <constructor-arg ref="tracer"/>
+  </bean>
+
+  <!-- traces all SQL calls -->
+  <bean id="censusSQLAspect" class="io.opencensus.contrib.spring.aop.CensusSpringSqlAspect">
+    <constructor-arg ref="tracer"/>
+  </bean>
+
+  <!-- global tracer -->
+  <bean id="tracer" class="io.opencensus.trace.Tracing" factory-method="getTracer"/>
+</beans>
+```
+
+### Traced Usage 
+
+Once configured, you can use the `@Traced` annotation to indicate that a method should 
+be wrapped with a `Span`.  By default, `@Traced` will use the name of the method as the
+span name.  However, `@Traced` supports an optional name attribute to allow a custom
+span name to be specified.
+
+```java
+  @Traced()
+  void example1() {
+    // do work
+  }
+  
+  // a custom span name can also be provided to Traced
+  @Traced(name = "custom-span-name")
+  void example2() {
+    // do moar work
+  }
+```
+
+#### Notes
+
+`opencensus-contrib-spring` support only enables annotations.  You will still need to configure opencensus and register exporters / views.
+
+[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-spring/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-spring
+
+#### Java Versions
+
+Java 6 or above is required for using this artifact.
+
+#### About the `aop` package
+
+`opencensus-contrib-spring` makes heavy use of Aspect Oriented Programming [AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming) to 
+add behavior to annotations.  Fortunately, Spring supports this natively so we can leverage the capabilities they've already built in. 
diff --git a/contrib/spring/build.gradle b/contrib/spring/build.gradle
new file mode 100644
index 0000000..941afcc
--- /dev/null
+++ b/contrib/spring/build.gradle
@@ -0,0 +1,21 @@
+description = 'OpenCensus Spring'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.spring_aspects,
+            libraries.spring_context
+
+    testCompile project(':opencensus-impl'),
+            project(':opencensus-testing'),
+            libraries.aspectj,
+            libraries.spring_test
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java
new file mode 100644
index 0000000..2edc57c
--- /dev/null
+++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018, 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.spring.aop;
+
+import io.opencensus.trace.Tracer;
+import java.lang.reflect.Method;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Configurable;
+
+/**
+ * CensusSpringAspect handles logic for the `@Traced` annotation.
+ *
+ * @since 0.16.0
+ */
+@Aspect
+@Configurable
+public final class CensusSpringAspect {
+  private final Tracer tracer;
+
+  /**
+   * Creates a {@code CensusSpringAspect} with the given tracer.
+   *
+   * @param tracer the tracer responsible for building new spans
+   * @since 0.16.0
+   */
+  public CensusSpringAspect(Tracer tracer) {
+    this.tracer = tracer;
+  }
+
+  /**
+   * trace handles methods executed with the `@Traced` annotation. A new span will be created with
+   * an optionally customizable span name.
+   *
+   * @param call the join point to execute
+   * @return the result of the invocation
+   * @throws Throwable if the underlying target throws an exception
+   * @since 0.16.0
+   */
+  @Around("@annotation(io.opencensus.contrib.spring.aop.Traced)")
+  public Object trace(ProceedingJoinPoint call) throws Throwable {
+    MethodSignature signature = (MethodSignature) call.getSignature();
+    Method method = signature.getMethod();
+
+    Traced annotation = method.getAnnotation(Traced.class);
+    if (annotation == null) {
+      return call.proceed();
+    }
+    String spanName = annotation.name();
+    if (spanName.isEmpty()) {
+      spanName = method.getName();
+    }
+
+    return Handler.proceed(call, tracer, spanName);
+  }
+}
diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSqlAspect.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSqlAspect.java
new file mode 100644
index 0000000..0fbd715
--- /dev/null
+++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSqlAspect.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018, 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.spring.aop;
+
+import io.opencensus.trace.Tracer;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.beans.factory.annotation.Configurable;
+
+/**
+ * CensusSpringSqlAspect captures span from all SQL invocations that utilize
+ * java.sql.Statement.execute*
+ *
+ * @since 0.16.0
+ */
+@Aspect
+@Configurable
+public final class CensusSpringSqlAspect {
+  private final Tracer tracer;
+
+  /**
+   * Creates a {@code CensusSpringSqlAspect} with the given tracer.
+   *
+   * @param tracer the tracer responsible for building new spans
+   * @since 0.16.0
+   */
+  public CensusSpringSqlAspect(Tracer tracer) {
+    this.tracer = tracer;
+  }
+
+  /**
+   * trace handles invocations of java.sql.Statement.execute*. A new span will be created whose name
+   * is (execute|executeQuery|executeQuery)-(hash of sql).
+   *
+   * @since 0.16.0
+   */
+  @Around("execute() || testing()")
+  public Object trace(ProceedingJoinPoint call) throws Throwable {
+    if (call.getArgs().length == 0 || call.getArgs()[0] == null) {
+      return call.proceed();
+    }
+
+    String sql = (String) call.getArgs()[0];
+    String spanName = makeSpanName(call, sql);
+
+    return Handler.proceed(call, tracer, spanName, sql);
+  }
+
+  /**
+   * execute creates spans around all invocations of Statement.execute*. The raw SQL will be stored
+   * in an annotation associated with the Span
+   */
+  @Pointcut("execution(public !void java.sql.Statement.execute*(java.lang.String))")
+  protected void execute() {}
+
+  @Pointcut("execution(public void Sample.execute*(java.lang.String))")
+  protected void testing() {}
+
+  private static String makeSpanName(ProceedingJoinPoint call, String sql) {
+    String hash = Integer.toHexString(hashCode(sql.toCharArray()));
+    return call.getSignature().getName() + "-" + hash;
+  }
+
+  private static int hashCode(char[] seq) {
+    if (seq == null) {
+      return 0;
+    }
+
+    int hash = 0;
+    for (char c : seq) {
+      hash = 31 * hash + c;
+    }
+    return hash;
+  }
+}
diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java
new file mode 100644
index 0000000..218854b
--- /dev/null
+++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018, 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.spring.aop;
+
+import io.opencensus.common.Scope;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import java.util.HashMap;
+import java.util.Map;
+import org.aspectj.lang.ProceedingJoinPoint;
+
+/** Handler defines common logic for wrapping a span around the specified JoinPoint. */
+final class Handler {
+  private Handler() {}
+
+  static Object proceed(
+      ProceedingJoinPoint call, Tracer tracer, String spanName, String... annotations)
+      throws Throwable {
+    Scope scope = tracer.spanBuilder(spanName).startScopedSpan();
+    try {
+      for (String annotation : annotations) {
+        tracer.getCurrentSpan().addAnnotation(annotation);
+      }
+
+      return call.proceed();
+
+    } catch (Throwable t) {
+      Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+      String message = t.getMessage();
+      attributes.put(
+          "message", AttributeValue.stringAttributeValue(message == null ? "null" : message));
+      attributes.put("type", AttributeValue.stringAttributeValue(t.getClass().toString()));
+
+      Span span = tracer.getCurrentSpan();
+      span.addAnnotation("error", attributes);
+      span.setStatus(Status.UNKNOWN);
+      throw t;
+    } finally {
+      scope.close();
+    }
+  }
+}
diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Traced.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Traced.java
new file mode 100644
index 0000000..51f7311
--- /dev/null
+++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Traced.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018, 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.spring.aop;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Traced specifies the annotated method should be included in the Trace.
+ *
+ * <p>By default, the name of the method will be used for the span name. However, the span name can
+ * be explicitly set via the name interface.
+ *
+ * @since 0.16.0
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Traced {
+
+  /**
+   * The optional custom span name.
+   *
+   * @return the optional custom span name; if not specified the method name will be used as the
+   *     span name
+   */
+  String name() default "";
+}
diff --git a/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringAspectTest.java b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringAspectTest.java
new file mode 100644
index 0000000..3e4415c
--- /dev/null
+++ b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringAspectTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2018, 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.spring.aop;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.testing.export.TestHandler;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+/**
+ * CensusSpringAspectTest verifies the weaving and application of the spring aop annotations.
+ *
+ * <p>Test Logic:
+ *
+ * <ol>
+ *   <li>Configure a simple bean, Sample, via spring.xml
+ *   <li>Include spring annotation support in spring.xml
+ *   <li>Use spring to load the Sample bean which will weave the census aspects into the bean.
+ *   <li>Use the TestHandler (defined in @Before and @After) to capture generated span.
+ *   <li>In each test, we verify the pointcuts are applied correctly by inspecting the span captured
+ *       in the TestHandler.
+ * </ol>
+ */
+@RunWith(JUnit4.class)
+public class CensusSpringAspectTest {
+  ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
+
+  private TestHandler handler;
+
+  @Before
+  public void setup() {
+    handler = new TestHandler();
+
+    SpanExporter exporter = Tracing.getExportComponent().getSpanExporter();
+    exporter.registerHandler("testing", handler);
+
+    TraceParams params =
+        Tracing.getTraceConfig()
+            .getActiveTraceParams()
+            .toBuilder()
+            .setSampler(Samplers.alwaysSample())
+            .build();
+    Tracing.getTraceConfig().updateActiveTraceParams(params);
+  }
+
+  @After
+  public void teardown() {
+    SpanExporter exporter = Tracing.getExportComponent().getSpanExporter();
+    exporter.unregisterHandler("testing");
+  }
+
+  @Test
+  public void tracedUsesMethodAsSpanName() throws Exception {
+    // When
+    Sample sample = (Sample) context.getBean("sample");
+    sample.call(100);
+
+    // Then
+    List<SpanData> data = handler.waitForExport(1);
+    assertThat(data).isNotNull();
+    assertThat(data.size()).isEqualTo(1);
+    assertThat(data.get(0).getName()).isEqualTo("call");
+  }
+
+  @Test
+  public void tracedAcceptsCustomSpanName() throws Exception {
+    // When
+    Sample sample = (Sample) context.getBean("sample");
+    sample.custom(100);
+
+    // Then
+    List<SpanData> data = handler.waitForExport(1);
+    assertThat(data).isNotNull();
+    assertThat(data.size()).isEqualTo(1);
+    assertThat(data.get(0).getName()).isEqualTo("blah");
+  }
+
+  @Test
+  public void handlesException() {
+    // When
+    Sample sample = (Sample) context.getBean("sample");
+    try {
+      sample.boom();
+    } catch (Exception ignored) {
+      //  ok
+    }
+
+    // Then
+    List<SpanData> spanList = handler.waitForExport(1);
+    assertThat(spanList).isNotNull();
+    assertThat(spanList.size()).isEqualTo(1);
+
+    SpanData spanData = spanList.get(0);
+    assertThat(spanData.getName()).isEqualTo("boom");
+    assertThat(spanData.getStatus()).isEqualTo(Status.UNKNOWN);
+
+    SpanData.TimedEvents<Annotation> annotations = spanData.getAnnotations();
+    assertThat(annotations).isNotNull();
+
+    List<SpanData.TimedEvent<Annotation>> events = annotations.getEvents();
+    assertThat(events.size()).isEqualTo(1);
+    assertThat(events.get(0).getEvent().getDescription()).isEqualTo("error");
+  }
+
+  @Test
+  public void sqlExecute() throws Exception {
+    // When
+    String sql = "select 1";
+    Sample sample = (Sample) context.getBean("sample");
+    sample.execute(sql);
+
+    // Then
+    List<SpanData> data = handler.waitForExport(1);
+    assertThat(data).isNotNull();
+    assertThat(data.size()).isEqualTo(1);
+    assertThat(data.get(0).getName()).isEqualTo("execute-4705ea0d"); // sql-{hash of sql statement}
+
+    List<SpanData.TimedEvent<Annotation>> events = data.get(0).getAnnotations().getEvents();
+    assertThat(events.size()).isEqualTo(1);
+    assertThat(events.get(0).getEvent().getDescription()).isEqualTo(sql);
+  }
+
+  @Test
+  public void sqlQuery() throws Exception {
+    // When
+    String sql = "select 2";
+    Sample sample = (Sample) context.getBean("sample");
+    sample.executeQuery(sql);
+
+    // Then
+    List<SpanData> data = handler.waitForExport(1);
+    assertThat(data).isNotNull();
+    assertThat(data.size()).isEqualTo(1);
+    assertThat(data.get(0).getName()).isEqualTo("executeQuery-4705ea0e");
+
+    SpanData.TimedEvents<Annotation> annotations = data.get(0).getAnnotations();
+    List<SpanData.TimedEvent<Annotation>> events = annotations.getEvents();
+    assertThat(events.size()).isEqualTo(1);
+    assertThat(events.get(0).getEvent().getDescription()).isEqualTo(sql);
+  }
+
+  @Test
+  public void sqlUpdate() throws Exception {
+    // When
+    String sql = "update content set value = 1";
+    Sample sample = (Sample) context.getBean("sample");
+    sample.executeUpdate(sql);
+
+    // Then
+    List<SpanData> data = handler.waitForExport(1);
+    assertThat(data).isNotNull();
+    assertThat(data.size()).isEqualTo(1);
+    assertThat(data.get(0).getName()).isEqualTo("executeUpdate-acaeb423");
+
+    List<SpanData.TimedEvent<Annotation>> events = data.get(0).getAnnotations().getEvents();
+    assertThat(events.size()).isEqualTo(1);
+    assertThat(events.get(0).getEvent().getDescription()).isEqualTo(sql);
+  }
+}
diff --git a/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java
new file mode 100644
index 0000000..87cb94f
--- /dev/null
+++ b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.spring.aop;
+
+import java.sql.SQLException;
+
+public class Sample {
+  @Traced()
+  void example1() {
+    // do work
+  }
+
+  @Traced(name = "custom-span-name")
+  void example2() {
+    // do moar work
+  }
+
+  @Traced()
+  void call(long delay) throws Exception {
+    Thread.sleep(delay);
+  }
+
+  @Traced(name = "blah")
+  void custom(long delay) throws Exception {
+    Thread.sleep(delay);
+  }
+
+  @Traced()
+  void boom() throws Exception {
+    throw new Exception("boom");
+  }
+
+  public void execute(String sql) throws SQLException {}
+
+  public void executeQuery(String sql) throws SQLException {}
+
+  public void executeUpdate(String sql) throws SQLException {}
+
+  public void executeLargeUpdate(String sql) throws SQLException {}
+}
diff --git a/contrib/spring/src/test/resources/spring.xml b/contrib/spring/src/test/resources/spring.xml
new file mode 100644
index 0000000..729c2e6
--- /dev/null
+++ b/contrib/spring/src/test/resources/spring.xml
@@ -0,0 +1,23 @@
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:aop="http://www.springframework.org/schema/aop"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
+
+  <!-- register the bean we'll use for testing -->
+  <bean id="sample" class="io.opencensus.contrib.spring.aop.Sample"/>
+
+  <aop:aspectj-autoproxy/>
+
+  <!-- traces explicit calls to @Traced -->
+  <bean id="censusAspect" class="io.opencensus.contrib.spring.aop.CensusSpringAspect">
+    <constructor-arg ref="tracer"/>
+  </bean>
+
+  <!-- traces all SQL calls -->
+  <bean id="censusSQLAspect" class="io.opencensus.contrib.spring.aop.CensusSpringSqlAspect">
+    <constructor-arg ref="tracer"/>
+  </bean>
+
+  <!-- global tracer -->
+  <bean id="tracer" class="io.opencensus.trace.Tracing" factory-method="getTracer"/>
+</beans>
diff --git a/contrib/spring_sleuth_v1x/README.md b/contrib/spring_sleuth_v1x/README.md
new file mode 100644
index 0000000..3345783
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/README.md
@@ -0,0 +1,52 @@
+# OpenCensus Spring Sleuth
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Spring Sleuth for Java* is a library for automatically
+propagating the OpenCensus trace context when working with [Spring Sleuth][spring-sleuth-url].
+
+This is an __experimental component__, please bring feedback to
+https://gitter.im/census-instrumentation/Lobby not the usual
+sleuth channel https://gitter.im/spring-cloud/spring-cloud-sleuth.
+
+This version is compatible with [Spring Boot 1.5.x][spring-boot-1.5-url].
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-spring-sleuth</artifactId>
+    <version>0.16.1</version>
+    <exclusions>
+      <exclusion>
+	    <groupId>org.springframework.cloud</groupId>
+	    <artifactId>spring-cloud-build</artifactId>
+	  </exclusion>
+	  <exclusion>
+	    <groupId>org.springframework.cloud</groupId>
+	    <artifactId>spring-cloud-starter-sleuth</artifactId>
+   	  </exclusion>
+    </exclusions>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-spring-sleuth:0.16.1'
+```
+
+[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-spring-sleuth/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-spring-sleuth
+[spring-boot-1.5-url]: https://github.com/spring-projects/spring-boot/tree/1.5.x
+[spring-sleuth-url]: https://github.com/spring-cloud/spring-cloud-sleuth
diff --git a/contrib/spring_sleuth_v1x/build.gradle b/contrib/spring_sleuth_v1x/build.gradle
new file mode 100644
index 0000000..53ff1c0
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/build.gradle
@@ -0,0 +1,21 @@
+description = 'OpenCensus Spring Sleuth'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+	    libraries.spring_boot_starter_web,
+	    libraries.spring_cloud_build,
+	    libraries.spring_cloud_starter_sleuth
+
+    testCompile project(':opencensus-impl'),
+            project(':opencensus-testing'),
+            libraries.spring_test
+
+    signature "org.codehaus.mojo.signature:java16:+@signature"
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthAutoConfiguration.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthAutoConfiguration.java
new file mode 100644
index 0000000..de4201f
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthAutoConfiguration.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018, 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.spring.sleuth.v1x;
+
+import io.opencensus.common.ExperimentalApi;
+import java.util.Random;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.sleuth.Sampler;
+import org.springframework.cloud.sleuth.SpanNamer;
+import org.springframework.cloud.sleuth.SpanReporter;
+import org.springframework.cloud.sleuth.TraceKeys;
+import org.springframework.cloud.sleuth.Tracer;
+import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
+import org.springframework.cloud.sleuth.log.SpanLogger;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.annotation.Role;
+
+/**
+ * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} that
+ * allows inter-operation between Sleuth(Brave) and OpenCensus.
+ *
+ * @since 0.16
+ */
+@ExperimentalApi
+@Configuration
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+@ConditionalOnProperty(name = "spring.opencensus.sleuth.enabled", matchIfMissing = true)
+@AutoConfigureBefore(TraceAutoConfiguration.class)
+@EnableConfigurationProperties(OpenCensusSleuthProperties.class)
+public class OpenCensusSleuthAutoConfiguration {
+
+  @Bean
+  @Primary
+  Tracer openCensusSleuthTracer(
+      Sampler sampler,
+      Random random,
+      SpanNamer spanNamer,
+      SpanLogger spanLogger,
+      SpanReporter spanReporter,
+      TraceKeys traceKeys) {
+    return new OpenCensusSleuthTracer(
+        sampler, random, spanNamer, spanLogger, spanReporter, traceKeys, /* traceId128= */ true);
+  }
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthProperties.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthProperties.java
new file mode 100644
index 0000000..5cd0e57
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthProperties.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018, 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.spring.sleuth.v1x;
+
+import io.opencensus.common.ExperimentalApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Sleuth annotation settings.
+ *
+ * @since 0.16
+ */
+@ExperimentalApi
+@ConfigurationProperties("spring.opencensus.sleuth")
+public class OpenCensusSleuthProperties {
+
+  private boolean enabled = true;
+
+  /** Returns whether OpenCensus trace propagation is enabled. */
+  public boolean isEnabled() {
+    return this.enabled;
+  }
+
+  /** Enables OpenCensus trace propagation. */
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
+  }
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpan.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpan.java
new file mode 100644
index 0000000..eeacfcb
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpan.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018, 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.spring.sleuth.v1x;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import java.nio.ByteBuffer;
+import java.util.EnumSet;
+import java.util.Map;
+
+/**
+ * Implementaion of Span that is created from a Sleuth Span.
+ *
+ * @since 0.16
+ */
+@ExperimentalApi
+public class OpenCensusSleuthSpan extends Span {
+
+  private static final EnumSet<Options> recordOptions = EnumSet.of(Options.RECORD_EVENTS);
+  private static final EnumSet<Options> notRecordOptions = EnumSet.noneOf(Options.class);
+
+  private static final TraceOptions sampledOptions =
+      TraceOptions.builder().setIsSampled(true).build();
+  private static final TraceOptions notSampledOptions =
+      TraceOptions.builder().setIsSampled(false).build();
+
+  OpenCensusSleuthSpan(org.springframework.cloud.sleuth.Span span) {
+    super(
+        fromSleuthSpan(span),
+        Boolean.TRUE.equals(span.isExportable()) ? recordOptions : notRecordOptions);
+  }
+
+  @Override
+  public void addAnnotation(String s, Map<String, AttributeValue> map) {}
+
+  @Override
+  public void addAnnotation(Annotation annotation) {}
+
+  @Override
+  public void addLink(Link link) {}
+
+  @Override
+  public void end(EndSpanOptions endSpanOptions) {}
+
+  // TODO: upgrade to new SpanContext.create() once it has been released.
+  @SuppressWarnings("deprecation")
+  private static SpanContext fromSleuthSpan(org.springframework.cloud.sleuth.Span span) {
+    return SpanContext.create(
+        TraceId.fromBytes(
+            ByteBuffer.allocate(TraceId.SIZE)
+                .putLong(span.getTraceIdHigh())
+                .putLong(span.getTraceId())
+                .array()),
+        SpanId.fromBytes(ByteBuffer.allocate(SpanId.SIZE).putLong(span.getSpanId()).array()),
+        Boolean.TRUE.equals(span.isExportable()) ? sampledOptions : notSampledOptions);
+  }
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolder.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolder.java
new file mode 100644
index 0000000..db6a355
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolder.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2018, 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.spring.sleuth.v1x;
+
+import io.grpc.Context;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.trace.unsafe.ContextUtils;
+import org.apache.commons.logging.Log;
+import org.springframework.cloud.sleuth.Span;
+import org.springframework.core.NamedThreadLocal;
+
+/*>>>
+  import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Inspired by the Sleuth's {@code SpanContextHolder}. */
+@ExperimentalApi
+final class OpenCensusSleuthSpanContextHolder {
+  private static final Log log =
+      org.apache.commons.logging.LogFactory.getLog(OpenCensusSleuthSpanContextHolder.class);
+  private static final ThreadLocal</*@Nullable*/ SpanContext> CURRENT_SPAN =
+      new NamedThreadLocal</*@Nullable*/ SpanContext>("Trace Context");
+
+  // Get the current span out of the thread context.
+  @javax.annotation.Nullable
+  static Span getCurrentSpan() {
+    SpanContext currentSpanContext = CURRENT_SPAN.get();
+    return currentSpanContext != null ? currentSpanContext.span : null;
+  }
+
+  // Set the current span in the thread context
+  static void setCurrentSpan(Span span) {
+    if (log.isTraceEnabled()) {
+      log.trace("Setting current span " + span);
+    }
+    push(span, /* autoClose= */ false);
+  }
+
+  // Remove all thread context relating to spans (useful for testing).
+  // See close() for a better alternative in instrumetation
+  static void removeCurrentSpan() {
+    removeCurrentSpanInternal(null);
+  }
+
+  @SuppressWarnings("CheckReturnValue")
+  @javax.annotation.Nullable
+  private static SpanContext removeCurrentSpanInternal(
+      @javax.annotation.Nullable SpanContext toRestore) {
+    if (toRestore != null) {
+      setSpanContextInternal(toRestore);
+    } else {
+      CURRENT_SPAN.remove();
+      // This is a big hack and can cause other data in the io.grpc.Context to be lost. But
+      // Spring 1.5 does not use io.grpc.Context and because the framework does not accept any
+      // gRPC context, the context will always be ROOT anyway.
+      Context.ROOT.attach();
+    }
+    return toRestore;
+  }
+
+  // Check if there is already a span in the current thread.
+  static boolean isTracing() {
+    return CURRENT_SPAN.get() != null;
+  }
+
+  // Close the current span and all parents that can be auto closed. On every iteration a function
+  // will be applied on the closed Span.
+  static void close(SpanFunction spanFunction) {
+    SpanContext current = CURRENT_SPAN.get();
+    while (current != null) {
+      spanFunction.apply(current.span);
+      current = removeCurrentSpanInternal(current.parent);
+      if (current == null || !current.autoClose) {
+        return;
+      }
+    }
+  }
+
+  // Close the current span and all parents that can be auto closed.
+  static void close() {
+    close(NO_OP_FUNCTION);
+  }
+
+  /**
+   * Push a span into the thread context, with the option to have it auto close if any child spans
+   * are themselves closed. Use autoClose=true if you start a new span with a parent that wasn't
+   * already in thread context.
+   */
+  static void push(Span span, boolean autoClose) {
+    if (isCurrent(span)) {
+      return;
+    }
+    setSpanContextInternal(new SpanContext(span, autoClose));
+  }
+
+  interface SpanFunction {
+    void apply(Span span);
+  }
+
+  private static final SpanFunction NO_OP_FUNCTION =
+      new SpanFunction() {
+        @Override
+        public void apply(Span span) {}
+      };
+
+  @SuppressWarnings("CheckReturnValue")
+  private static void setSpanContextInternal(SpanContext spanContext) {
+    CURRENT_SPAN.set(spanContext);
+    spanContext.ocCurrentContext.attach();
+  }
+
+  private static boolean isCurrent(Span span) {
+    if (span == null) {
+      return false;
+    }
+    SpanContext currentSpanContext = CURRENT_SPAN.get();
+    return currentSpanContext != null && span.equals(currentSpanContext.span);
+  }
+
+  private static class SpanContext {
+    final Span span;
+    final boolean autoClose;
+    @javax.annotation.Nullable final SpanContext parent;
+    final OpenCensusSleuthSpan ocSpan;
+    final Context ocCurrentContext;
+
+    private SpanContext(Span span, boolean autoClose) {
+      this.span = span;
+      this.autoClose = autoClose;
+      this.parent = CURRENT_SPAN.get();
+      this.ocSpan = new OpenCensusSleuthSpan(span);
+      this.ocCurrentContext =
+          Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, this.ocSpan);
+    }
+  }
+
+  private OpenCensusSleuthSpanContextHolder() {}
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracer.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracer.java
new file mode 100644
index 0000000..bba9bab
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracer.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2018, 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.spring.sleuth.v1x;
+
+import io.opencensus.common.ExperimentalApi;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.cloud.sleuth.Sampler;
+import org.springframework.cloud.sleuth.Span;
+import org.springframework.cloud.sleuth.SpanNamer;
+import org.springframework.cloud.sleuth.SpanReporter;
+import org.springframework.cloud.sleuth.TraceKeys;
+import org.springframework.cloud.sleuth.Tracer;
+import org.springframework.cloud.sleuth.instrument.async.SpanContinuingTraceCallable;
+import org.springframework.cloud.sleuth.instrument.async.SpanContinuingTraceRunnable;
+import org.springframework.cloud.sleuth.log.SpanLogger;
+import org.springframework.cloud.sleuth.util.ExceptionUtils;
+import org.springframework.cloud.sleuth.util.SpanNameUtil;
+
+/*>>>
+  import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Sleuth Tracer that keeps a synchronized OpenCensus Span. This class is based on Sleuth's {@code
+ * DefaultTracer}.
+ *
+ * @since 0.16
+ */
+@ExperimentalApi
+public class OpenCensusSleuthTracer implements Tracer {
+  private static final Log log = LogFactory.getLog(OpenCensusSleuthTracer.class);
+  private final Sampler defaultSampler;
+  private final Random random;
+  private final SpanNamer spanNamer;
+  private final SpanLogger spanLogger;
+  private final SpanReporter spanReporter;
+  private final TraceKeys traceKeys;
+  private final boolean traceId128;
+
+  /** Basic constructor holding components for implementing Sleuth's {@link Tracer} interface. */
+  public OpenCensusSleuthTracer(
+      Sampler defaultSampler,
+      Random random,
+      SpanNamer spanNamer,
+      SpanLogger spanLogger,
+      SpanReporter spanReporter,
+      TraceKeys traceKeys) {
+    this(
+        defaultSampler,
+        random,
+        spanNamer,
+        spanLogger,
+        spanReporter,
+        traceKeys,
+        /* traceId128= */ false);
+  }
+
+  /** Basic constructor holding components for implementing Sleuth's {@link Tracer} interface. */
+  public OpenCensusSleuthTracer(
+      Sampler defaultSampler,
+      Random random,
+      SpanNamer spanNamer,
+      SpanLogger spanLogger,
+      SpanReporter spanReporter,
+      TraceKeys traceKeys,
+      boolean traceId128) {
+    this.defaultSampler = defaultSampler;
+    this.random = random;
+    this.spanNamer = spanNamer;
+    this.spanLogger = spanLogger;
+    this.spanReporter = spanReporter;
+    this.traceId128 = traceId128;
+    this.traceKeys = traceKeys != null ? traceKeys : new TraceKeys();
+  }
+
+  @Override
+  @javax.annotation.Nullable
+  public Span createSpan(String name, /*@Nullable*/ Span parent) {
+    if (parent == null) {
+      return createSpan(name);
+    }
+    return continueSpan(createChild(parent, name));
+  }
+
+  @Override
+  @javax.annotation.Nullable
+  public Span createSpan(String name) {
+    return this.createSpan(name, this.defaultSampler);
+  }
+
+  @Override
+  @javax.annotation.Nullable
+  public Span createSpan(String name, /*@Nullable*/ Sampler sampler) {
+    String shortenedName = SpanNameUtil.shorten(name);
+    Span span;
+    if (isTracing()) {
+      span = createChild(getCurrentSpan(), shortenedName);
+    } else {
+      long id = createId();
+      span =
+          Span.builder()
+              .name(shortenedName)
+              .traceIdHigh(this.traceId128 ? createTraceIdHigh() : 0L)
+              .traceId(id)
+              .spanId(id)
+              .build();
+      if (sampler == null) {
+        sampler = this.defaultSampler;
+      }
+      span = sampledSpan(span, sampler);
+      this.spanLogger.logStartedSpan(null, span);
+    }
+    return continueSpan(span);
+  }
+
+  @Override
+  @javax.annotation.Nullable
+  public Span detach(/*@Nullable*/ Span span) {
+    if (span == null) {
+      return null;
+    }
+    Span current = OpenCensusSleuthSpanContextHolder.getCurrentSpan();
+    if (current == null) {
+      if (log.isTraceEnabled()) {
+        log.trace(
+            "Span in the context is null so something has already detached the span. "
+                + "Won't do anything about it");
+      }
+      return null;
+    }
+    if (!span.equals(current)) {
+      ExceptionUtils.warn(
+          "Tried to detach trace span but "
+              + "it is not the current span: "
+              + span
+              + ". You may have forgotten to close or detach "
+              + current);
+    } else {
+      OpenCensusSleuthSpanContextHolder.removeCurrentSpan();
+    }
+    return span.getSavedSpan();
+  }
+
+  @Override
+  @javax.annotation.Nullable
+  public Span close(/*@Nullable*/ Span span) {
+    if (span == null) {
+      return null;
+    }
+    final Span savedSpan = span.getSavedSpan();
+    Span current = OpenCensusSleuthSpanContextHolder.getCurrentSpan();
+    if (current == null || !span.equals(current)) {
+      ExceptionUtils.warn(
+          "Tried to close span but it is not the current span: "
+              + span
+              + ".  You may have forgotten to close or detach "
+              + current);
+    } else {
+      span.stop();
+      if (savedSpan != null && span.getParents().contains(savedSpan.getSpanId())) {
+        this.spanReporter.report(span);
+        this.spanLogger.logStoppedSpan(savedSpan, span);
+      } else {
+        if (!span.isRemote()) {
+          this.spanReporter.report(span);
+          this.spanLogger.logStoppedSpan(null, span);
+        }
+      }
+      OpenCensusSleuthSpanContextHolder.close(
+          new OpenCensusSleuthSpanContextHolder.SpanFunction() {
+            @Override
+            public void apply(Span closedSpan) {
+              // Note: hasn't this already been done?
+              OpenCensusSleuthTracer.this.spanLogger.logStoppedSpan(savedSpan, closedSpan);
+            }
+          });
+    }
+    return savedSpan;
+  }
+
+  Span createChild(/*@Nullable*/ Span parent, String name) {
+    String shortenedName = SpanNameUtil.shorten(name);
+    long id = createId();
+    if (parent == null) {
+      Span span =
+          Span.builder()
+              .name(shortenedName)
+              .traceIdHigh(this.traceId128 ? createTraceIdHigh() : 0L)
+              .traceId(id)
+              .spanId(id)
+              .build();
+      span = sampledSpan(span, this.defaultSampler);
+      this.spanLogger.logStartedSpan(null, span);
+      return span;
+    } else {
+      if (!isTracing()) {
+        OpenCensusSleuthSpanContextHolder.push(parent, /* autoClose= */ true);
+      }
+      Span span =
+          Span.builder()
+              .name(shortenedName)
+              .traceIdHigh(parent.getTraceIdHigh())
+              .traceId(parent.getTraceId())
+              .parent(parent.getSpanId())
+              .spanId(id)
+              .processId(parent.getProcessId())
+              .savedSpan(parent)
+              .exportable(parent.isExportable())
+              .baggage(parent.getBaggage())
+              .build();
+      this.spanLogger.logStartedSpan(parent, span);
+      return span;
+    }
+  }
+
+  private static Span sampledSpan(Span span, Sampler sampler) {
+    if (!sampler.isSampled(span)) {
+      // Copy everything, except set exportable to false
+      return Span.builder()
+          .begin(span.getBegin())
+          .traceIdHigh(span.getTraceIdHigh())
+          .traceId(span.getTraceId())
+          .spanId(span.getSpanId())
+          .name(span.getName())
+          .exportable(false)
+          .build();
+    }
+    return span;
+  }
+
+  // Encodes a timestamp into the upper 32-bits, so that it can be converted to an Amazon trace ID.
+  // For example, an Amazon trace ID is composed of the following:
+  //  |-- 32 bits for epoch seconds -- | -- 96 bits for random data -- |
+  //
+  // To support this, Span#getTraceIdHigh() holds the epoch seconds and first 32 random bits: and
+  // Span#getTraceId() holds the remaining 64 random bits.
+  private long createTraceIdHigh() {
+    long epochSeconds = System.currentTimeMillis() / 1000;
+    int random = this.random.nextInt();
+    return (epochSeconds & 0xffffffffL) << 32 | (random & 0xffffffffL);
+  }
+
+  private long createId() {
+    return this.random.nextLong();
+  }
+
+  @Override
+  @javax.annotation.Nullable
+  public Span continueSpan(/*@Nullable*/ Span span) {
+    if (span != null) {
+      this.spanLogger.logContinuedSpan(span);
+    } else {
+      return null;
+    }
+    Span newSpan = createContinuedSpan(span, OpenCensusSleuthSpanContextHolder.getCurrentSpan());
+    OpenCensusSleuthSpanContextHolder.setCurrentSpan(newSpan);
+    return newSpan;
+  }
+
+  @SuppressWarnings("deprecation")
+  private static Span createContinuedSpan(Span span, /*@Nullable*/ Span saved) {
+    if (saved == null && span.getSavedSpan() != null) {
+      saved = span.getSavedSpan();
+    }
+    return new Span(span, saved);
+  }
+
+  @Override
+  @javax.annotation.Nullable
+  public Span getCurrentSpan() {
+    return OpenCensusSleuthSpanContextHolder.getCurrentSpan();
+  }
+
+  @Override
+  public boolean isTracing() {
+    return OpenCensusSleuthSpanContextHolder.isTracing();
+  }
+
+  @Override
+  public void addTag(String key, String value) {
+    Span s = getCurrentSpan();
+    if (s != null && s.isExportable()) {
+      s.tag(key, value);
+    }
+  }
+
+  /**
+   * Wrap the callable in a TraceCallable, if tracing.
+   *
+   * @return The callable provided, wrapped if tracing, 'callable' if not.
+   */
+  @Override
+  public <V> Callable<V> wrap(Callable<V> callable) {
+    if (isTracing()) {
+      return new SpanContinuingTraceCallable<V>(this, this.traceKeys, this.spanNamer, callable);
+    }
+    return callable;
+  }
+
+  /**
+   * Wrap the runnable in a TraceRunnable, if tracing.
+   *
+   * @return The runnable provided, wrapped if tracing, 'runnable' if not.
+   */
+  @Override
+  public Runnable wrap(Runnable runnable) {
+    if (isTracing()) {
+      return new SpanContinuingTraceRunnable(this, this.traceKeys, this.spanNamer, runnable);
+    }
+    return runnable;
+  }
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/contrib/spring_sleuth_v1x/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/resources/META-INF/additional-spring-configuration-metadata.json
diff --git a/contrib/spring_sleuth_v1x/src/main/resources/META-INF/spring.factories b/contrib/spring_sleuth_v1x/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..5e65451
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,7 @@
+# Auto Configuration
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+io.opencensus.contrib.spring.sleuth.v1x.OpenCensusSleuthAutoConfiguration\
+
+# Environment Post Processor
+org.springframework.boot.env.EnvironmentPostProcessor=\
+org.springframework.cloud.sleuth.autoconfig.TraceEnvironmentPostProcessor
diff --git a/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolderTest.java b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolderTest.java
new file mode 100644
index 0000000..997ed4f
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolderTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018, 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.spring.sleuth.v1x;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link OpenCensusSleuthSpanContextHolder}. */
+@RunWith(JUnit4.class)
+public class OpenCensusSleuthSpanContextHolderTest {
+  private static final Tracer tracer = Tracing.getTracer();
+
+  @After
+  @Before
+  public void verifyNotTracing() {
+    assertThat(OpenCensusSleuthSpanContextHolder.isTracing()).isFalse();
+    assertThat(tracer.getCurrentSpan().getContext().isValid()).isFalse();
+  }
+
+  @Test
+  public void testFromSleuthSampled() {
+    org.springframework.cloud.sleuth.Span sleuthSpan =
+        createSleuthSpan(21, 22, 23, /* exportable= */ true);
+    OpenCensusSleuthSpanContextHolder.setCurrentSpan(sleuthSpan);
+    assertThat(OpenCensusSleuthSpanContextHolder.isTracing()).isTrue();
+    assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpan);
+    assertSpanEquals(tracer.getCurrentSpan(), sleuthSpan);
+    assertThat(tracer.getCurrentSpan().getContext().getTraceOptions().isSampled()).isTrue();
+    OpenCensusSleuthSpanContextHolder.close();
+  }
+
+  @Test
+  public void testFromSleuthUnsampled() {
+    org.springframework.cloud.sleuth.Span sleuthSpan =
+        createSleuthSpan(21, 22, 23, /* exportable= */ false);
+    OpenCensusSleuthSpanContextHolder.setCurrentSpan(sleuthSpan);
+    assertThat(OpenCensusSleuthSpanContextHolder.isTracing()).isTrue();
+    assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpan);
+    assertSpanEquals(tracer.getCurrentSpan(), sleuthSpan);
+    assertThat(tracer.getCurrentSpan().getContext().getTraceOptions().isSampled()).isFalse();
+    OpenCensusSleuthSpanContextHolder.close();
+  }
+
+  @Test
+  public void testSpanStackSimple() {
+    org.springframework.cloud.sleuth.Span[] sleuthSpans = createSleuthSpans(4);
+    // push all the spans
+    for (int i = 0; i < sleuthSpans.length; i++) {
+      OpenCensusSleuthSpanContextHolder.push(sleuthSpans[i], /* autoClose= */ false);
+      assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpans[i]);
+      assertSpanEquals(tracer.getCurrentSpan(), sleuthSpans[i]);
+    }
+    // pop all the spans
+    for (int i = sleuthSpans.length - 1; i >= 0; i--) {
+      assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpans[i]);
+      assertSpanEquals(tracer.getCurrentSpan(), sleuthSpans[i]);
+      OpenCensusSleuthSpanContextHolder.close();
+    }
+  }
+
+  @Test
+  public void testSpanStackAutoClose() {
+    org.springframework.cloud.sleuth.Span[] sleuthSpans = createSleuthSpans(4);
+    // push all the spans
+    for (int i = 0; i < sleuthSpans.length; i++) {
+      // set autoclose for all the spans except 2
+      OpenCensusSleuthSpanContextHolder.push(sleuthSpans[i], /* autoClose= */ i != 2);
+      assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpans[i]);
+      assertSpanEquals(tracer.getCurrentSpan(), sleuthSpans[i]);
+    }
+    // verify autoClose pops stack to index 2
+    OpenCensusSleuthSpanContextHolder.close();
+    assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpans[2]);
+    assertSpanEquals(tracer.getCurrentSpan(), sleuthSpans[2]);
+    // verify autoClose closes pops rest of stack
+    OpenCensusSleuthSpanContextHolder.close();
+  }
+
+  @Test
+  public void testSpanStackCloseSpanFunction() {
+    final org.springframework.cloud.sleuth.Span[] sleuthSpans = createSleuthSpans(4);
+    // push all the spans
+    for (int i = 0; i < sleuthSpans.length; i++) {
+      OpenCensusSleuthSpanContextHolder.push(sleuthSpans[i], /* autoClose= */ false);
+    }
+    // pop all the spans, verify that given SpanFunction is called on the closed span.
+    for (int i = sleuthSpans.length - 1; i >= 0; i--) {
+      final int index = i;
+      OpenCensusSleuthSpanContextHolder.close(
+          new OpenCensusSleuthSpanContextHolder.SpanFunction() {
+            @Override
+            public void apply(org.springframework.cloud.sleuth.Span span) {
+              assertThat(span).isEqualTo(sleuthSpans[index]);
+            }
+          });
+    }
+  }
+
+  org.springframework.cloud.sleuth.Span[] createSleuthSpans(int len) {
+    org.springframework.cloud.sleuth.Span[] spans = new org.springframework.cloud.sleuth.Span[len];
+    for (int i = 0; i < len; i++) {
+      spans[i] = createSleuthSpan(i * 10 + 1, i * 10 + 2, i * 10 + 3, /* exportable= */ true);
+    }
+    return spans;
+  }
+
+  private static org.springframework.cloud.sleuth.Span createSleuthSpan(
+      long tidHi, long tidLo, long sid, boolean exportable) {
+    return org.springframework.cloud.sleuth.Span.builder()
+        .name("name")
+        .traceIdHigh(tidHi)
+        .traceId(tidLo)
+        .spanId(sid)
+        .exportable(exportable)
+        .build();
+  }
+
+  private static void assertSpanEquals(
+      Span span, org.springframework.cloud.sleuth.Span sleuthSpan) {
+    assertThat(Long.parseLong(span.getContext().getTraceId().toLowerBase16().substring(0, 16), 16))
+        .isEqualTo(sleuthSpan.getTraceIdHigh());
+    assertThat(Long.parseLong(span.getContext().getTraceId().toLowerBase16().substring(16, 32), 16))
+        .isEqualTo(sleuthSpan.getTraceId());
+    assertThat(Long.parseLong(span.getContext().getSpanId().toLowerBase16(), 16))
+        .isEqualTo(sleuthSpan.getSpanId());
+    assertThat(span.getContext().getTraceOptions().isSampled())
+        .isEqualTo(sleuthSpan.isExportable());
+  }
+}
diff --git a/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanTest.java b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanTest.java
new file mode 100644
index 0000000..a4a04f9
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018, 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.spring.sleuth.v1x;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.springframework.cloud.sleuth.Span;
+
+/** Unit tests for {@link OpenCensusSleuthSpan}. */
+@RunWith(JUnit4.class)
+public class OpenCensusSleuthSpanTest {
+  @Test
+  public void testFromSleuthSampled() {
+    Span sleuthSpan =
+        Span.builder()
+            .name("name")
+            .traceIdHigh(12L)
+            .traceId(22L)
+            .spanId(23L)
+            .exportable(true)
+            .build();
+    assertSpanEquals(new OpenCensusSleuthSpan(sleuthSpan), sleuthSpan);
+  }
+
+  @Test
+  public void testFromSleuthNotSampled() {
+    Span sleuthSpan =
+        Span.builder()
+            .name("name")
+            .traceIdHigh(12L)
+            .traceId(22L)
+            .spanId(23L)
+            .exportable(false)
+            .build();
+    assertSpanEquals(new OpenCensusSleuthSpan(sleuthSpan), sleuthSpan);
+  }
+
+  private static final void assertSpanEquals(io.opencensus.trace.Span span, Span sleuthSpan) {
+    assertThat(span.getContext().isValid()).isTrue();
+    assertThat(Long.parseLong(span.getContext().getTraceId().toLowerBase16().substring(0, 16), 16))
+        .isEqualTo(sleuthSpan.getTraceIdHigh());
+    assertThat(Long.parseLong(span.getContext().getTraceId().toLowerBase16().substring(16, 32), 16))
+        .isEqualTo(sleuthSpan.getTraceId());
+    assertThat(Long.parseLong(span.getContext().getSpanId().toLowerBase16(), 16))
+        .isEqualTo(sleuthSpan.getSpanId());
+    assertThat(span.getContext().getTraceOptions().isSampled())
+        .isEqualTo(sleuthSpan.isExportable());
+  }
+}
diff --git a/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracerTest.java b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracerTest.java
new file mode 100644
index 0000000..924c06e
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracerTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2018, 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.spring.sleuth.v1x;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Random;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.springframework.cloud.sleuth.DefaultSpanNamer;
+import org.springframework.cloud.sleuth.NoOpSpanReporter;
+import org.springframework.cloud.sleuth.Span;
+import org.springframework.cloud.sleuth.TraceKeys;
+import org.springframework.cloud.sleuth.Tracer;
+import org.springframework.cloud.sleuth.log.NoOpSpanLogger;
+import org.springframework.cloud.sleuth.sampler.AlwaysSampler;
+
+/** Unit tests for {@link OpenCensusSleuthTracer}. */
+@RunWith(JUnit4.class)
+public class OpenCensusSleuthTracerTest {
+  private static final Tracer tracer =
+      new OpenCensusSleuthTracer(
+          new AlwaysSampler(),
+          new Random(),
+          new DefaultSpanNamer(),
+          new NoOpSpanLogger(),
+          new NoOpSpanReporter(),
+          new TraceKeys());
+
+  @After
+  @Before
+  public void verifyNotTracing() {
+    assertThat(tracer.isTracing()).isFalse();
+  }
+
+  @Test
+  public void testRootSpanAndClose() {
+    Span root = tracer.createSpan("root");
+    assertCurrentSpanIs(root);
+    assertThat(root.getSavedSpan()).isNull();
+    Span parent = tracer.close(root);
+    assertThat(parent).isNull();
+  }
+
+  @Test
+  public void testSpanStackAndClose() {
+    Span[] spans = createSpansAndAssertCurrent(3);
+    // pop the stack
+    for (int i = spans.length - 1; i >= 0; i--) {
+      assertCurrentSpanIs(spans[i]);
+      Span parent = tracer.close(spans[i]);
+      assertThat(parent).isEqualTo(spans[i].getSavedSpan());
+    }
+  }
+
+  @Test
+  public void testSpanStackAndCloseOutOfOrder() {
+    Span[] spans = createSpansAndAssertCurrent(3);
+    // try to close a non-current span
+    tracer.close(spans[spans.length - 2]);
+    assertCurrentSpanIs(spans[spans.length - 1]);
+    // pop the stack
+    for (int i = spans.length - 1; i >= 0; i--) {
+      tracer.close(spans[i]);
+    }
+  }
+
+  @Test
+  public void testDetachNull() {
+    Span parent = tracer.detach(null);
+    assertThat(parent).isNull();
+  }
+
+  @Test
+  public void testRootSpanAndDetach() {
+    Span root = tracer.createSpan("root");
+    assertCurrentSpanIs(root);
+    assertThat(root.getSavedSpan()).isNull();
+    Span parent = tracer.detach(root);
+    assertThat(parent).isNull();
+  }
+
+  @Test
+  public void testSpanStackAndDetach() {
+    Span[] spans = createSpansAndAssertCurrent(3);
+    Span parent = tracer.detach(spans[spans.length - 1]);
+    assertThat(parent).isEqualTo(spans[spans.length - 2]);
+  }
+
+  @Test
+  public void testSpanStackAndDetachOutOfOrder() {
+    Span[] spans = createSpansAndAssertCurrent(3);
+    // try to detach a non-current span
+    tracer.detach(spans[spans.length - 2]);
+    assertCurrentSpanIs(spans[spans.length - 1]);
+    Span parent = tracer.detach(spans[spans.length - 1]);
+    assertThat(parent).isEqualTo(spans[spans.length - 2]);
+  }
+
+  @Test
+  public void testContinueNull() {
+    Span span = tracer.continueSpan(null);
+    assertThat(span).isNull();
+  }
+
+  @Test
+  public void testRootSpanAndContinue() {
+    Span root = tracer.createSpan("root");
+    assertCurrentSpanIs(root);
+    tracer.detach(root);
+    Span span = tracer.continueSpan(root);
+    assertThat(span).isEqualTo(root);
+    tracer.detach(span);
+  }
+
+  @Test
+  public void testSpanStackAndContinue() {
+    Span[] spans = createSpansAndAssertCurrent(3);
+    Span original = tracer.getCurrentSpan();
+    assertThat(original).isEqualTo(spans[spans.length - 1]);
+    Span parent = tracer.detach(original);
+    assertThat(parent).isEqualTo(spans[spans.length - 2]);
+    assertThat(tracer.getCurrentSpan()).isNull();
+
+    Span continued = tracer.continueSpan(original);
+    assertCurrentSpanIs(continued);
+    assertThat(continued.getSavedSpan()).isEqualTo(parent);
+    assertThat(continued).isEqualTo(original);
+    tracer.detach(continued);
+  }
+
+  @Test
+  public void testSpanStackAndCreateAndContinue() {
+    createSpansAndAssertCurrent(3);
+    Span original = tracer.getCurrentSpan();
+    tracer.detach(original);
+    Span root = tracer.createSpan("root");
+    assertCurrentSpanIs(root);
+    Span continued = tracer.continueSpan(original);
+    assertCurrentSpanIs(continued);
+    assertThat(continued.getSavedSpan()).isEqualTo(root);
+    assertThat(continued).isEqualTo(original);
+    assertThat(continued.getSavedSpan()).isNotEqualTo(original.getSavedSpan());
+    tracer.detach(continued);
+  }
+
+  // Verifies span and associated saved span.
+  private static void assertCurrentSpanIs(Span span) {
+    assertThat(tracer.getCurrentSpan()).isEqualTo(span);
+    assertThat(tracer.getCurrentSpan().getSavedSpan()).isEqualTo(span.getSavedSpan());
+
+    assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(span);
+    assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan().getSavedSpan())
+        .isEqualTo(span.getSavedSpan());
+  }
+
+  private static Span[] createSpansAndAssertCurrent(int len) {
+    Span[] spans = new Span[len];
+
+    Span current = null;
+    for (int i = 0; i < len; i++) {
+      current = tracer.createSpan("span" + i, current);
+      spans[i] = current;
+      assertCurrentSpanIs(current);
+    }
+    return spans;
+  }
+}
diff --git a/contrib/zpages/README.md b/contrib/zpages/README.md
new file mode 100644
index 0000000..2a535ce
--- /dev/null
+++ b/contrib/zpages/README.md
@@ -0,0 +1,97 @@
+# OpenCensus Z-Pages
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Z-Pages for Java* is a collection of HTML pages to display stats and trace data and
+allows library configuration control.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-contrib-zpages</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-contrib-zpages:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+### Register the Z-Pages
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) throws Exception {
+    ZPageHandlers.startHttpServerAndRegisterAll(8080);
+    // ... do work
+  }
+}
+```
+
+### View stats and spans on Z-Pages
+
+#### View RPC stats on /rpcz page
+
+The /rpcz page displays the canonical gRPC cumulative and interval stats broken down by RPC methods.
+Example:
+
+![rpcz-example](screenshots/rpcz-example.png)
+
+#### View measures and stats for all exported views on /statsz page
+
+The /statsz page displays measures and stats for all exported views. Views are grouped into directories 
+according to their namespace. Example:
+
+![statsz-example-1](screenshots/statsz-example-1.png)
+![statsz-example-2](screenshots/statsz-example-2.png)
+
+#### View trace spans on /tracez page
+
+The /tracez page displays information about all active spans and all sampled spans based on latency 
+and errors. Example:
+
+![tracez-example](screenshots/tracez-example.png)
+
+#### View and update tracing configuration on /traceconfigz page
+
+The /traceconfigz page displays information about the current active tracing configuration and 
+allows users to change it. Example:
+
+![traceconfigz-example](screenshots/traceconfigz-example.png)
+
+
+### FAQ
+
+#### Why do I not see sampled spans based on latency and error codes for a given span name?
+Sampled spans based on latency and error codes are available only for registered span names. 
+For more details see [SampledSpanStore][sampledspanstore-url].
+
+[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-zpages/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-zpages
+[sampledspanstore-url]: https://github.com/census-instrumentation/opencensus-java/blob/master/api/src/main/java/io/opencensus/trace/export/SampledSpanStore.java
diff --git a/contrib/zpages/build.gradle b/contrib/zpages/build.gradle
new file mode 100644
index 0000000..9648d64
--- /dev/null
+++ b/contrib/zpages/build.gradle
@@ -0,0 +1,16 @@
+description = 'OpenCensus Z-Pages'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.8
+    it.targetCompatibility = 1.8
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            project(':opencensus-contrib-grpc-metrics',),
+            libraries.guava
+
+    signature "org.codehaus.mojo.signature:java18:+@signature"
+}
diff --git a/contrib/zpages/screenshots/rpcz-example.png b/contrib/zpages/screenshots/rpcz-example.png
new file mode 100644
index 0000000..9d303fb
--- /dev/null
+++ b/contrib/zpages/screenshots/rpcz-example.png
Binary files differ
diff --git a/contrib/zpages/screenshots/statsz-example-1.png b/contrib/zpages/screenshots/statsz-example-1.png
new file mode 100644
index 0000000..503a05b
--- /dev/null
+++ b/contrib/zpages/screenshots/statsz-example-1.png
Binary files differ
diff --git a/contrib/zpages/screenshots/statsz-example-2.png b/contrib/zpages/screenshots/statsz-example-2.png
new file mode 100644
index 0000000..bb1229c
--- /dev/null
+++ b/contrib/zpages/screenshots/statsz-example-2.png
Binary files differ
diff --git a/contrib/zpages/screenshots/traceconfigz-example.png b/contrib/zpages/screenshots/traceconfigz-example.png
new file mode 100644
index 0000000..5428768
--- /dev/null
+++ b/contrib/zpages/screenshots/traceconfigz-example.png
Binary files differ
diff --git a/contrib/zpages/screenshots/tracez-example.png b/contrib/zpages/screenshots/tracez-example.png
new file mode 100644
index 0000000..cfcf0f3
--- /dev/null
+++ b/contrib/zpages/screenshots/tracez-example.png
Binary files differ
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/RpczZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/RpczZPageHandler.java
new file mode 100644
index 0000000..4d79fb0
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/RpczZPageHandler.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2018, 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.zpages;
+
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_CUMULATIVE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_STARTED_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_ERROR_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_ERROR_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_ERROR_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_FINISHED_COUNT_CUMULATIVE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_FINISHED_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_ELAPSED_TIME_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_LATENCY_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_LATENCY_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_STARTED_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_STARTED_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagValue;
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedMap;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** HTML page formatter for gRPC cumulative and interval stats. */
+@SuppressWarnings("deprecation")
+final class RpczZPageHandler extends ZPageHandler {
+
+  private final ViewManager viewManager;
+
+  private static final String RPCZ_URL = "/rpcz";
+  private static final String SENT = "Sent";
+  private static final String RECEIVED = "Received";
+  private static final double SECONDS_PER_MINUTE = 60.0;
+  private static final double SECONDS_PER_HOUR = 3600.0;
+  private static final double NANOS_PER_SECOND = 1e9;
+  private static final double BYTES_PER_KB = 1024;
+  private static final ImmutableList<String> RPC_STATS_TYPES =
+      ImmutableList.of(
+          "Count",
+          "Avg latency (ms)",
+          // TODO(songya): add a column for latency percentiles.
+          "Rate (rpc/s)",
+          "Input (kb/s)",
+          "Output (kb/s)",
+          "Errors");
+
+  private static final ImmutableList<View> CLIENT_RPC_CUMULATIVE_VIEWS =
+      ImmutableList.of(
+          RPC_CLIENT_ERROR_COUNT_VIEW,
+          RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW,
+          RPC_CLIENT_REQUEST_BYTES_VIEW,
+          RPC_CLIENT_RESPONSE_BYTES_VIEW,
+          RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW,
+          // The last 5 views are not used yet.
+          RPC_CLIENT_REQUEST_COUNT_VIEW,
+          RPC_CLIENT_RESPONSE_COUNT_VIEW,
+          RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW,
+          RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW,
+          RPC_CLIENT_FINISHED_COUNT_CUMULATIVE_VIEW);
+
+  private static final ImmutableList<View> SERVER_RPC_CUMULATIVE_VIEWS =
+      ImmutableList.of(
+          RPC_SERVER_ERROR_COUNT_VIEW,
+          RPC_SERVER_SERVER_LATENCY_VIEW,
+          RPC_SERVER_REQUEST_BYTES_VIEW,
+          RPC_SERVER_RESPONSE_BYTES_VIEW,
+          RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW,
+          // The last 5 views are not used yet.
+          RPC_SERVER_REQUEST_COUNT_VIEW,
+          RPC_SERVER_RESPONSE_COUNT_VIEW,
+          RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW,
+          RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW,
+          RPC_SERVER_FINISHED_COUNT_CUMULATIVE_VIEW);
+
+  // Interval views may be removed in the future.
+  private static final ImmutableList<View> CLIENT_RPC_MINUTE_VIEWS =
+      ImmutableList.of(
+          RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW,
+          RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW,
+          RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW,
+          RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW,
+          RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW,
+          // The last 5 views are not used yet.
+          RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW,
+          RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW,
+          RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW,
+          RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW,
+          RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW);
+
+  // Interval views may be removed in the future.
+  private static final ImmutableList<View> SERVER_RPC_MINUTE_VIEWS =
+      ImmutableList.of(
+          RPC_SERVER_ERROR_COUNT_MINUTE_VIEW,
+          RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW,
+          RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW,
+          RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW,
+          RPC_SERVER_STARTED_COUNT_MINUTE_VIEW,
+          // The last 5 views are not used yet.
+          RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW,
+          RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW,
+          RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW,
+          RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW,
+          RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW);
+
+  // Interval views may be removed in the future.
+  private static final ImmutableList<View> CLIENT_RPC_HOUR_VIEWS =
+      ImmutableList.of(
+          RPC_CLIENT_ERROR_COUNT_HOUR_VIEW,
+          RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW,
+          RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW,
+          RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW,
+          RPC_CLIENT_STARTED_COUNT_HOUR_VIEW,
+          // The last 5 views are not used yet.
+          RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW,
+          RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW,
+          RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW,
+          RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW,
+          RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW);
+
+  // Interval views may be removed in the future.
+  private static final ImmutableList<View> SERVER_RPC_HOUR_VIEWS =
+      ImmutableList.of(
+          RPC_SERVER_ERROR_COUNT_HOUR_VIEW,
+          RPC_SERVER_SERVER_LATENCY_HOUR_VIEW,
+          RPC_SERVER_SERVER_ELAPSED_TIME_HOUR_VIEW,
+          RPC_SERVER_REQUEST_BYTES_HOUR_VIEW,
+          RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW,
+          RPC_SERVER_STARTED_COUNT_HOUR_VIEW,
+          // The last 5 views are not used yet.
+          RPC_SERVER_REQUEST_COUNT_HOUR_VIEW,
+          RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW,
+          RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW,
+          RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW,
+          RPC_SERVER_FINISHED_COUNT_HOUR_VIEW);
+
+  @Override
+  public String getUrlPath() {
+    return RPCZ_URL;
+  }
+
+  private static void emitStyle(PrintWriter out) {
+    out.write("<style>\n");
+    out.write(Style.style);
+    out.write("</style>\n");
+  }
+
+  @Override
+  public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
+    PrintWriter out =
+        new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
+    out.write("<!DOCTYPE html>\n");
+    out.write("<html lang=\"en\"><head>\n");
+    out.write("<meta charset=\"utf-8\">\n");
+    out.write("<title>RpcZ</title>\n");
+    out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
+    out.write(
+        "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+            + "rel=\"stylesheet\">\n");
+    out.write(
+        "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
+    emitStyle(out);
+    out.write("</head>\n");
+    out.write("<body>\n");
+    try {
+      emitHtmlBody(out);
+    } catch (Throwable t) {
+      out.write("Errors while generate the HTML page " + t);
+    }
+    out.write("</body>\n");
+    out.write("</html>\n");
+    out.close();
+  }
+
+  private void emitHtmlBody(PrintWriter out) {
+    Formatter formatter = new Formatter(out, Locale.US);
+    out.write(
+        "<p class=\"header\">"
+            + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
+            + "Open<span>Census</span></p>");
+    out.write("<h1>RPC Stats</h1>");
+    out.write("<p></p>");
+    emitSummaryTable(out, formatter, /* isReceived= */ false);
+    emitSummaryTable(out, formatter, /* isReceived= */ true);
+  }
+
+  private void emitSummaryTable(PrintWriter out, Formatter formatter, boolean isReceived) {
+    formatter.format(
+        "<h2><table class=\"title\"><tr align=left><td><font size=+2>"
+            + "%s</font></td></tr></table></h2>",
+        (isReceived ? RECEIVED : SENT));
+    formatter.format("<table frame=box cellspacing=0 cellpadding=2>");
+    emitSummaryTableHeader(out, formatter);
+    Map<String, StatsSnapshot> snapshots = getStatsSnapshots(isReceived);
+    for (Entry<String, StatsSnapshot> entry : snapshots.entrySet()) {
+      emitSummaryTableRows(out, formatter, entry.getValue(), entry.getKey());
+    }
+    out.write("</table>");
+    out.write("<br />");
+  }
+
+  private static void emitSummaryTableHeader(PrintWriter out, Formatter formatter) {
+    // First line.
+    formatter.format("<tr bgcolor=#A94442>");
+    out.write("<th></th><td></td>");
+    for (String rpcStatsType : RPC_STATS_TYPES) {
+      formatter.format("<th class=\"borderLB\" colspan=3>%s</th>", rpcStatsType);
+    }
+    out.write("</tr>");
+
+    // Second line.
+    formatter.format("<tr bgcolor=#A94442>");
+    out.write("<th align=left>Method</th>\n");
+    out.write("<td bgcolor=#A94442>&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+    for (int i = 0; i < RPC_STATS_TYPES.size(); i++) {
+      out.write("<th class=\"borderLB\" align=center>Min.</th>\n");
+      out.write("<th class=\"borderLB\" align=center>Hr.</th>\n");
+      out.write("<th class=\"borderLB\" align=center>Tot.</th>");
+    }
+  }
+
+  private static void emitSummaryTableRows(
+      PrintWriter out, Formatter formatter, StatsSnapshot snapshot, String method) {
+    out.write("<tr>");
+    formatter.format("<td><b>%s</b></td>", method);
+    out.write("<td></td>");
+    formatter.format("<td class=\"borderLC\">%d</td>", snapshot.countLastMinute);
+    formatter.format("<td class=\"borderLC\">%d</td>", snapshot.countLastHour);
+    formatter.format("<td class=\"borderLC\">%d</td>", snapshot.countTotal);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.avgLatencyLastMinute);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.avgLatencyLastHour);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.avgLatencyTotal);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.rpcRateLastMinute);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.rpcRateLastHour);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.rpcRateTotal);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.inputRateLastMinute);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.inputRateLastHour);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.inputRateTotal);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.outputRateLastMinute);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.outputRateLastHour);
+    formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.outputRateTotal);
+    formatter.format("<td class=\"borderLC\">%d</td>", snapshot.errorsLastMinute);
+    formatter.format("<td class=\"borderLC\">%d</td>", snapshot.errorsLastHour);
+    formatter.format("<td class=\"borderLC\">%d</td>", snapshot.errorsTotal);
+    out.write("</tr>");
+  }
+
+  // Gets stats snapshot for each method.
+  private Map<String, StatsSnapshot> getStatsSnapshots(boolean isReceived) {
+    SortedMap<String, StatsSnapshot> map = Maps.newTreeMap(); // Sorted by method name.
+    if (isReceived) {
+      getStatsSnapshots(map, SERVER_RPC_CUMULATIVE_VIEWS);
+      getStatsSnapshots(map, SERVER_RPC_MINUTE_VIEWS);
+      getStatsSnapshots(map, SERVER_RPC_HOUR_VIEWS);
+    } else {
+      getStatsSnapshots(map, CLIENT_RPC_CUMULATIVE_VIEWS);
+      getStatsSnapshots(map, CLIENT_RPC_MINUTE_VIEWS);
+      getStatsSnapshots(map, CLIENT_RPC_HOUR_VIEWS);
+    }
+    return map;
+  }
+
+  private void getStatsSnapshots(Map<String, StatsSnapshot> map, List<View> views) {
+    for (View view : views) {
+      ViewData viewData = viewManager.getView(view.getName());
+      if (viewData == null) {
+        continue;
+      }
+      for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+          viewData.getAggregationMap().entrySet()) {
+        TagValue tagValue;
+        List</*@Nullable*/ TagValue> tagValues = entry.getKey();
+        if (tagValues.size() == 1) {
+          tagValue = tagValues.get(0);
+        } else { // Error count views have two tag key: status and method.
+          tagValue = tagValues.get(1);
+        }
+        String method = tagValue == null ? "" : tagValue.asString();
+        StatsSnapshot snapshot = map.get(method);
+        if (snapshot == null) {
+          snapshot = new StatsSnapshot();
+          map.put(method, snapshot);
+        }
+
+        getStats(snapshot, entry.getValue(), view, viewData.getWindowData());
+      }
+    }
+  }
+
+  // Gets RPC stats by its view definition, and set it to stats snapshot.
+  private static void getStats(
+      StatsSnapshot snapshot,
+      AggregationData data,
+      View view,
+      ViewData.AggregationWindowData windowData) {
+    if (view == RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW || view == RPC_SERVER_SERVER_LATENCY_VIEW) {
+      snapshot.avgLatencyTotal = ((DistributionData) data).getMean();
+    } else if (view == RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW
+        || view == RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW) {
+      snapshot.avgLatencyLastMinute = ((AggregationData.MeanData) data).getMean();
+    } else if (view == RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW
+        || view == RPC_SERVER_SERVER_LATENCY_HOUR_VIEW) {
+      snapshot.avgLatencyLastHour = ((AggregationData.MeanData) data).getMean();
+    } else if (view == RPC_CLIENT_ERROR_COUNT_VIEW || view == RPC_SERVER_ERROR_COUNT_VIEW) {
+      snapshot.errorsTotal = ((AggregationData.MeanData) data).getCount();
+    } else if (view == RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW
+        || view == RPC_SERVER_ERROR_COUNT_MINUTE_VIEW) {
+      snapshot.errorsLastMinute = ((AggregationData.MeanData) data).getCount();
+    } else if (view == RPC_CLIENT_ERROR_COUNT_HOUR_VIEW
+        || view == RPC_SERVER_ERROR_COUNT_HOUR_VIEW) {
+      snapshot.errorsLastHour = ((AggregationData.MeanData) data).getCount();
+    } else if (view == RPC_CLIENT_REQUEST_BYTES_VIEW || view == RPC_SERVER_REQUEST_BYTES_VIEW) {
+      DistributionData distributionData = (DistributionData) data;
+      snapshot.inputRateTotal =
+          distributionData.getCount()
+              * distributionData.getMean()
+              / BYTES_PER_KB
+              / getDurationInSecs((ViewData.AggregationWindowData.CumulativeData) windowData);
+    } else if (view == RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW
+        || view == RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW) {
+      AggregationData.MeanData meanData = (AggregationData.MeanData) data;
+      snapshot.inputRateLastMinute =
+          meanData.getMean() * meanData.getCount() / BYTES_PER_KB / SECONDS_PER_MINUTE;
+    } else if (view == RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW
+        || view == RPC_SERVER_REQUEST_BYTES_HOUR_VIEW) {
+      AggregationData.MeanData meanData = (AggregationData.MeanData) data;
+      snapshot.inputRateLastHour =
+          meanData.getMean() * meanData.getCount() / BYTES_PER_KB / SECONDS_PER_HOUR;
+    } else if (view == RPC_CLIENT_RESPONSE_BYTES_VIEW || view == RPC_SERVER_RESPONSE_BYTES_VIEW) {
+      DistributionData distributionData = (DistributionData) data;
+      snapshot.outputRateTotal =
+          distributionData.getCount()
+              * distributionData.getMean()
+              / BYTES_PER_KB
+              / getDurationInSecs((ViewData.AggregationWindowData.CumulativeData) windowData);
+    } else if (view == RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW
+        || view == RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW) {
+      AggregationData.MeanData meanData = (AggregationData.MeanData) data;
+      snapshot.outputRateLastMinute =
+          meanData.getMean() * meanData.getCount() / BYTES_PER_KB / SECONDS_PER_MINUTE;
+    } else if (view == RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW
+        || view == RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW) {
+      AggregationData.MeanData meanData = (AggregationData.MeanData) data;
+      snapshot.outputRateLastHour =
+          meanData.getMean() * meanData.getCount() / BYTES_PER_KB / SECONDS_PER_HOUR;
+    } else if (view == RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW
+        || view == RPC_SERVER_STARTED_COUNT_MINUTE_VIEW) {
+      snapshot.countLastMinute = ((CountData) data).getCount();
+      snapshot.rpcRateLastMinute = snapshot.countLastMinute / SECONDS_PER_MINUTE;
+    } else if (view == RPC_CLIENT_STARTED_COUNT_HOUR_VIEW
+        || view == RPC_SERVER_STARTED_COUNT_HOUR_VIEW) {
+      snapshot.countLastHour = ((CountData) data).getCount();
+      snapshot.rpcRateLastHour = snapshot.countLastHour / SECONDS_PER_HOUR;
+    } else if (view == RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW
+        || view == RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW) {
+      snapshot.countTotal = ((CountData) data).getCount();
+      snapshot.rpcRateTotal =
+          snapshot.countTotal
+              / getDurationInSecs((ViewData.AggregationWindowData.CumulativeData) windowData);
+    } // TODO(songya): compute and store latency percentiles.
+  }
+
+  // Calculates the duration of the given CumulativeData in seconds.
+  private static double getDurationInSecs(
+      ViewData.AggregationWindowData.CumulativeData cumulativeData) {
+    return toDoubleSeconds(cumulativeData.getEnd().subtractTimestamp(cumulativeData.getStart()));
+  }
+
+  // Converts a Duration to seconds. Converts the nanoseconds of the given duration to decimals of
+  // second, and adds it to the second of duration.
+  // For example, Duration.create(/* seconds */ 5, /* nanos */ 5 * 1e8) will be converted to 5.5
+  // seconds.
+  private static double toDoubleSeconds(Duration duration) {
+    return duration.getNanos() / NANOS_PER_SECOND + duration.getSeconds();
+  }
+
+  static RpczZPageHandler create(ViewManager viewManager) {
+    return new RpczZPageHandler(viewManager);
+  }
+
+  private RpczZPageHandler(ViewManager viewManager) {
+    this.viewManager = viewManager;
+  }
+
+  private static class StatsSnapshot {
+    long countLastMinute;
+    long countLastHour;
+    long countTotal;
+    double rpcRateLastMinute;
+    double rpcRateLastHour;
+    double rpcRateTotal;
+    double avgLatencyLastMinute;
+    double avgLatencyLastHour;
+    double avgLatencyTotal;
+    double inputRateLastMinute;
+    double inputRateLastHour;
+    double inputRateTotal;
+    double outputRateLastMinute;
+    double outputRateLastHour;
+    double outputRateTotal;
+    long errorsLastMinute;
+    long errorsLastHour;
+    long errorsTotal;
+  }
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/StatszZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/StatszZPageHandler.java
new file mode 100644
index 0000000..00c72d6
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/StatszZPageHandler.java
@@ -0,0 +1,629 @@
+/*
+ * Copyright 2018, 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.zpages;
+
+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.Charsets;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedMap;
+import javax.annotation.concurrent.GuardedBy;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** HTML page formatter for all exported {@link View}s. */
+@SuppressWarnings("deprecation")
+final class StatszZPageHandler extends ZPageHandler {
+
+  private static final Object monitor = new Object();
+
+  private final ViewManager viewManager;
+
+  // measures, cachedViews and root are created when StatszZPageHandler is initialized, and will
+  // be updated every time when there's a new View from viewManager.getAllExportedViews().
+  // viewManager.getAllExportedViews() will be called every time when the StatsZ page is
+  // re-rendered, like refreshing or navigating to other paths.
+
+  @GuardedBy("monitor")
+  private final Map<String, Measure> measures = Maps.newTreeMap();
+
+  @GuardedBy("monitor")
+  private final Set<View> cachedViews = Sets.newHashSet();
+
+  @GuardedBy("monitor")
+  private final TreeNode root = new TreeNode();
+
+  @VisibleForTesting static final String QUERY_PATH = "path";
+  private static final String STATSZ_URL = "/statsz";
+  private static final String CLASS_LARGER_TR = "directory-tr";
+  private static final String TABLE_HEADER_VIEW = "View Name";
+  private static final String TABLE_HEADER_DESCRIPTION = "Description";
+  private static final String TABLE_HEADER_MEASURE = "Measure";
+  private static final String TABLE_HEADER_AGGREGATION = "Aggregation Type";
+  private static final String TABLE_HEADER_START = "Start Time";
+  private static final String TABLE_HEADER_END = "End Time";
+  private static final String TABLE_HEADER_UNIT = "Unit";
+  private static final String TABLE_HEADER_MEASURE_TYPE = "Type";
+  private static final String TABLE_HEADER_SUM = "Sum";
+  private static final String TABLE_HEADER_COUNT = "Count";
+  private static final String TABLE_HEADER_MEAN = "Mean";
+  private static final String TABLE_HEADER_MAX = "Max";
+  private static final String TABLE_HEADER_MIN = "Min";
+  private static final String TABLE_HEADER_DEV = "Sum of Squared Deviations";
+  private static final String TABLE_HEADER_HISTOGRAM = "Histogram";
+  private static final String TABLE_HEADER_RANGE = "Range";
+  private static final String TABLE_HEADER_BUCKET_SIZE = "Bucket Size";
+  private static final String TABLE_HEADER_LAST_VALUE = "Last Value";
+  private static final long MILLIS_PER_SECOND = 1000;
+  private static final long NANOS_PER_MILLISECOND = 1000 * 1000;
+  private static final Splitter PATH_SPLITTER = Splitter.on('/');
+
+  @Override
+  public String getUrlPath() {
+    return STATSZ_URL;
+  }
+
+  @Override
+  public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
+    PrintWriter out =
+        new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
+    out.write("<!DOCTYPE html>\n");
+    out.write("<html lang=\"en\"><head>\n");
+    out.write("<meta charset=\"utf-8\">\n");
+    out.write("<title>StatsZ</title>\n");
+    out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
+    out.write(
+        "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+            + "rel=\"stylesheet\">\n");
+    out.write(
+        "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
+    Formatter formatter = new Formatter(out, Locale.US);
+    emitStyles(out, formatter);
+    out.write("</head>\n");
+    out.write("<body>\n");
+    try {
+      emitHtmlBody(queryMap, out, formatter);
+    } catch (Throwable t) {
+      out.write("Errors while generate the HTML page " + t);
+    }
+    out.write("</body>\n");
+    out.write("</html>\n");
+    out.close();
+  }
+
+  private static void emitStyles(PrintWriter out, Formatter formatter) {
+    out.write("<style>");
+    out.write(Style.style);
+    formatter.format(".%s{font-size:150%%}", CLASS_LARGER_TR);
+    out.write("</style>");
+  }
+
+  private void emitHtmlBody(Map<String, String> queryMap, PrintWriter out, Formatter formatter) {
+    synchronized (monitor) {
+      groupViewsByDirectoriesAndGetMeasures(
+          viewManager.getAllExportedViews(), root, measures, cachedViews);
+      out.write(
+          "<p class=\"header\">"
+              + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
+              + "Open<span>Census</span></p>");
+      out.write(
+          "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+              + "rel=\"stylesheet\">\n");
+      out.write(
+          "<link href=\"https://fonts.googleapis.com/css?family=Roboto\""
+              + "rel=\"stylesheet\">\n");
+      out.write("<h1><a href='?'>StatsZ</a></h1>");
+      out.write("<p></p>");
+      String path = queryMap.get(QUERY_PATH);
+      TreeNode current = findNode(path);
+      emitDirectoryTable(current, path, out, formatter);
+      if (current != null && current.viewName != null) {
+        ViewData viewData = viewManager.getView(current.viewName);
+        emitViewData(viewData, current.viewName, out, formatter);
+      }
+      emitMeasureTable(measures, out, formatter);
+    }
+  }
+
+  // Parses view names, creates a tree that represents the directory structure and put each view
+  // under appropriate directory. Also gets measures from the given views.
+  // Directories are the namespaces in view name, separated by '/'.
+  private static void groupViewsByDirectoriesAndGetMeasures(
+      Set<View> views, TreeNode root, Map<String, Measure> measures, Set<View> cachedViews) {
+    for (View view : views) {
+      if (cachedViews.contains(view)) {
+        continue;
+      }
+      cachedViews.add(view);
+
+      List<String> dirs = PATH_SPLITTER.splitToList(view.getName().asString());
+      TreeNode node = root;
+      for (int i = 0; i < dirs.size(); i++) {
+        if (node == null) {
+          break; // Should never happen. Work around the nullness checker.
+        }
+        String dir = dirs.get(i);
+        if ("".equals(dir) && i == 0) {
+          continue; // In case view name starts with a '/'.
+        }
+        node.views++;
+        if (i != dirs.size() - 1) { // Non-leaf node (directory node)
+          node.children.putIfAbsent(dir, new TreeNode());
+          node = node.children.get(dir);
+        } else { // Leaf node (view node)
+          node.children.putIfAbsent(dir, new TreeNode(view.getName()));
+        }
+      }
+
+      Measure measure = view.getMeasure();
+      measures.putIfAbsent(measure.getName(), measure);
+    }
+  }
+
+  @GuardedBy("monitor")
+  private void emitDirectoryTable(
+      /*@Nullable*/ TreeNode currentNode,
+      /*@Nullable*/ String path,
+      PrintWriter out,
+      Formatter formatter) {
+    out.write("<h2 style=\"margin-bottom:0;\">Views</h2>");
+    if (currentNode == null) {
+      formatter.format(
+          "<p><font size=+2>Directory not found: %s. Return to root.</font></p>", path);
+      currentNode = root;
+    }
+    if (currentNode == root || path == null) {
+      path = "";
+    }
+    emitDirectoryHeader(path, out, formatter);
+    out.write("<table class=\"title\" cellspacing=0 cellpadding=0>");
+    for (Entry<String, TreeNode> entry : currentNode.children.entrySet()) {
+      TreeNode child = entry.getValue();
+      String relativePath = entry.getKey();
+      if (child.viewName == null) { // Directory node, emit a row for directory.
+        formatter.format(
+            "<tr class=\"direct\"><td>Directory: <a href='?%s=%s'>%s</a> (%d %s)</td></tr>",
+            QUERY_PATH,
+            path + '/' + relativePath,
+            relativePath,
+            child.views,
+            child.views > 1 ? "views" : "view");
+      } else { // View node, emit a row for view.
+        String viewName = child.viewName.asString();
+        formatter.format(
+            "<tr class=\"direct\"><td>View: <a href='?%s=%s'>%s</a></td></tr>",
+            QUERY_PATH, path + '/' + relativePath, viewName);
+      }
+    }
+    out.write("</table>");
+    out.write("<p></p>");
+  }
+
+  // Searches the TreeNode whose absolute path matches the given path, started from root.
+  // Returns null if such a TreeNode doesn't exist.
+  @GuardedBy("monitor")
+  private /*@Nullable*/ TreeNode findNode(/*@Nullable*/ String path) {
+    if (Strings.isNullOrEmpty(path) || "/".equals(path)) { // Go back to the root directory.
+      return root;
+    } else {
+      List<String> dirs = PATH_SPLITTER.splitToList(path);
+      TreeNode node = root;
+      for (int i = 0; i < dirs.size(); i++) {
+        String dir = dirs.get(i);
+        if ("".equals(dir) && i == 0) {
+          continue; // Skip the first "", the path of root node.
+        }
+        if (!node.children.containsKey(dir)) {
+          return null;
+        } else {
+          node = node.children.get(dir);
+        }
+      }
+      return node;
+    }
+  }
+
+  private static void emitDirectoryHeader(String path, PrintWriter out, Formatter formatter) {
+    List<String> dirs = PATH_SPLITTER.splitToList(path);
+    StringBuilder currentPath = new StringBuilder("");
+    out.write("<h3>Current Path: ");
+    for (int i = 0; i < dirs.size(); i++) {
+      String dir = dirs.get(i);
+      currentPath.append(dir);
+      // create links to navigate back to parent directories.
+      formatter.format("<a href='?%s=%s'>%s</a>", QUERY_PATH, currentPath.toString(), dir + '/');
+      currentPath.append('/');
+    }
+    out.write("</h3>");
+  }
+
+  private static void emitViewData(
+      /*@Nullable*/ ViewData viewData, View.Name viewName, PrintWriter out, Formatter formatter) {
+    if (viewData == null) {
+      formatter.format("<p class=\"view\">No Stats found for View %s.</p>", viewName.asString());
+      return;
+    }
+    View view = viewData.getView();
+    emitViewInfo(view, viewData.getWindowData(), out, formatter);
+    formatter.format("<p class=\"view\">Stats for View: %s</p>", view.getName().asString());
+
+    formatter.format("<table cellspacing=0 cellpadding=0>");
+    emitViewDataTableHeader(view, out, formatter);
+    for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+        viewData.getAggregationMap().entrySet()) {
+      emitViewDataRow(view, entry, out, formatter);
+    }
+    out.write("</table>");
+    out.write("<p></p>");
+  }
+
+  private static void emitViewInfo(
+      View view, ViewData.AggregationWindowData windowData, PrintWriter out, Formatter formatter) {
+    formatter.format("<table width=100%% cellspacing=0 cellpadding=0>");
+    emitViewInfoHeader(out, formatter);
+
+    out.write("<tbody>");
+    out.write("<tr>"); // One row that represents the selected view.
+    formatter.format("<td>%s</td>", view.getName().asString());
+    formatter.format("<td class=\"borderLL\">%s</td>", view.getDescription());
+    formatter.format("<td class=\"borderLL\">%s</td>", view.getMeasure().getName());
+    String aggregationType =
+        view.getAggregation()
+            .match(
+                Functions.returnConstant("Sum"),
+                Functions.returnConstant("Count"),
+                Functions.returnConstant("Distribution"),
+                Functions.returnConstant("Last Value"),
+                new Function<Aggregation, String>() {
+                  @Override
+                  public String apply(Aggregation arg) {
+                    // TODO(songya): remove this once Mean aggregation is completely removed. Before
+                    // that
+                    // we need to continue supporting Mean, since it could still be used by users
+                    // and some
+                    // deprecated RPC views.
+                    if (arg instanceof Aggregation.Mean) {
+                      return "Mean";
+                    }
+                    throw new AssertionError();
+                  }
+                });
+    formatter.format("<td class=\"borderLL\">%s</td>", aggregationType);
+    windowData.match(
+        new Function<ViewData.AggregationWindowData.CumulativeData, Void>() {
+          @Override
+          public Void apply(ViewData.AggregationWindowData.CumulativeData arg) {
+            formatter.format("<td class=\"borderLL\">%s</td>", toDate(arg.getStart()));
+            formatter.format("<td class=\"borderLL\">%s</td>", toDate(arg.getEnd()));
+            return null;
+          }
+        },
+        Functions.</*@Nullable*/ Void>throwAssertionError(), // No interval views will be displayed.
+        Functions.</*@Nullable*/ Void>throwAssertionError());
+    out.write("</tr>");
+    out.write("</tbody>");
+    out.write("</table>");
+    out.write("<p></p>");
+  }
+
+  private static Date toDate(Timestamp timestamp) {
+    return Date.from(
+        Instant.ofEpochMilli(
+            timestamp.getSeconds() * MILLIS_PER_SECOND
+                + timestamp.getNanos() / NANOS_PER_MILLISECOND));
+  }
+
+  private static void emitViewInfoHeader(PrintWriter out, Formatter formatter) {
+    out.write("<thead>");
+    out.write("<tr>");
+    formatter.format("<th colspan=1 align=left>%s</th>", TABLE_HEADER_VIEW);
+    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_DESCRIPTION);
+    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_MEASURE);
+    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_AGGREGATION);
+    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_START);
+    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_END);
+    out.write("</tr>");
+    out.write("</thead>");
+  }
+
+  private static void emitViewDataTableHeader(View view, PrintWriter out, Formatter formatter) {
+    out.write("<thead>");
+    out.write("<tr>");
+    for (TagKey tagKey : view.getColumns()) {
+      formatter.format("<th class=\"borderRL\">TagKey: %s (string)</th>", tagKey.getName());
+    }
+    String unit = view.getMeasure().getUnit();
+    view.getAggregation()
+        .match(
+            new Function<Sum, Void>() {
+              @Override
+              public Void apply(Sum arg) {
+                formatter.format("<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_SUM, unit);
+                return null;
+              }
+            },
+            new Function<Count, Void>() {
+              @Override
+              public Void apply(Count arg) {
+                formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_COUNT);
+                return null;
+              }
+            },
+            new Function<Distribution, Void>() {
+              @Override
+              public Void apply(Distribution arg) {
+                formatter.format("<th>%s, %s</th>", TABLE_HEADER_MEAN, unit);
+                formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_COUNT);
+                formatter.format("<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_MAX, unit);
+                formatter.format("<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_MIN, unit);
+                formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_DEV);
+                formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_HISTOGRAM);
+                return null;
+              }
+            },
+            new Function<LastValue, Void>() {
+              @Override
+              public Void apply(LastValue arg) {
+                formatter.format(
+                    "<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_LAST_VALUE, unit);
+                return null;
+              }
+            },
+            new Function<Aggregation, Void>() {
+              @Override
+              public Void apply(Aggregation arg) {
+                // TODO(songya): remove this once Mean aggregation is completely removed. Before
+                // that
+                // we need to continue supporting Mean, since it could still be used by users and
+                // some
+                // deprecated RPC views.
+                if (arg instanceof Aggregation.Mean) {
+                  formatter.format("<th>%s, %s</th>", TABLE_HEADER_MEAN, unit);
+                  formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_COUNT);
+                  return null;
+                }
+                throw new IllegalArgumentException("Unknown Aggregation.");
+              }
+            });
+    out.write("</tr>");
+    out.write("</thead>");
+  }
+
+  private static void emitViewDataRow(
+      View view,
+      Entry<List</*@Nullable*/ TagValue>, AggregationData> entry,
+      PrintWriter out,
+      Formatter formatter) {
+    out.write("<tr>");
+    for (/*@Nullable*/ TagValue tagValue : entry.getKey()) {
+      String tagValueStr = tagValue == null ? "" : tagValue.asString();
+      formatter.format("<td class=\"borderRL\">%s</td>", tagValueStr);
+    }
+    entry
+        .getValue()
+        .match(
+            new Function<SumDataDouble, Void>() {
+              @Override
+              public Void apply(SumDataDouble arg) {
+                formatter.format("<td class=\"borderLL\">%.3f</td>", arg.getSum());
+                return null;
+              }
+            },
+            new Function<SumDataLong, Void>() {
+              @Override
+              public Void apply(SumDataLong arg) {
+                formatter.format("<td class=\"borderLL\">%d</td>", arg.getSum());
+                return null;
+              }
+            },
+            new Function<CountData, Void>() {
+              @Override
+              public Void apply(CountData arg) {
+                formatter.format("<td class=\"borderLL\">%d</td>", arg.getCount());
+                return null;
+              }
+            },
+            new Function<DistributionData, Void>() {
+              @Override
+              public Void apply(DistributionData arg) {
+                checkArgument(
+                    view.getAggregation() instanceof Distribution, "Distribution expected.");
+                formatter.format("<td>%.3f</td>", arg.getMean());
+                formatter.format("<td class=\"borderLL\">%d</td>", arg.getCount());
+                formatter.format("<td class=\"borderLL\">%.3f</td>", arg.getMax());
+                formatter.format("<td class=\"borderLL\">%.3f</td>", arg.getMin());
+                formatter.format(
+                    "<td class=\"borderLL\">%.3f</td>", arg.getSumOfSquaredDeviations());
+                emitHistogramBuckets(
+                    ((Distribution) view.getAggregation()).getBucketBoundaries().getBoundaries(),
+                    arg.getBucketCounts(),
+                    out,
+                    formatter);
+                return null;
+              }
+            },
+            new Function<LastValueDataDouble, Void>() {
+              @Override
+              public Void apply(LastValueDataDouble arg) {
+                formatter.format("<td>%.3f</td>", arg.getLastValue());
+                return null;
+              }
+            },
+            new Function<LastValueDataLong, Void>() {
+              @Override
+              public Void apply(LastValueDataLong arg) {
+                formatter.format("<td>%d</td>", arg.getLastValue());
+                return null;
+              }
+            },
+            new Function<AggregationData, Void>() {
+              @Override
+              public Void apply(AggregationData arg) {
+                if (arg instanceof AggregationData.MeanData) {
+                  AggregationData.MeanData meanData = (AggregationData.MeanData) arg;
+                  formatter.format("<td>%.3f</td>", meanData.getMean());
+                  formatter.format("<td class=\"borderLL\">%d</td>", meanData.getCount());
+                  return null;
+                }
+                throw new IllegalArgumentException("Unknown Aggregation.");
+              }
+            });
+    out.write("</tr>");
+  }
+
+  private static void emitHistogramBuckets(
+      List<Double> bucketBoundaries,
+      List<Long> bucketCounts,
+      PrintWriter out,
+      Formatter formatter) {
+    checkArgument(
+        bucketBoundaries.size() == bucketCounts.size() - 1,
+        "Bucket boundaries and counts don't match");
+    out.write("<td class=\"borderLL\">");
+    out.write("<table>");
+    formatter.format(
+        "<thead><tr><th>%s</th><th>%s</th></tr></thead>",
+        TABLE_HEADER_RANGE, TABLE_HEADER_BUCKET_SIZE);
+    out.write("<tbody>");
+    for (int i = 0; i < bucketCounts.size(); i++) {
+      double low = i == 0 ? Double.NEGATIVE_INFINITY : bucketBoundaries.get(i - 1);
+      double high =
+          i == bucketCounts.size() - 1 ? Double.POSITIVE_INFINITY : bucketBoundaries.get(i);
+      out.write("<tr>");
+      formatter.format("<td>[%.3f...%.3f)</td>", low, high);
+      formatter.format("<td>%d</td>", bucketCounts.get(i));
+      out.write("</tr>");
+    }
+    out.write("</tbody>");
+    out.write("</table>");
+    out.write("</td>");
+  }
+
+  private static void emitMeasureTable(
+      Map<String, Measure> measures, PrintWriter out, Formatter formatter) {
+    out.write("<h2>Measures with Views</h2>");
+    out.write("<p>Below are the measures used in registered views.</p>");
+    out.write("<p></p>");
+    formatter.format("<table cellspacing=0 cellpadding=0>");
+    emitMeasureTableHeader(out, formatter);
+    out.write("<tbody>");
+    for (Entry<String, Measure> entry : measures.entrySet()) {
+      emitMeasureTableRow(entry.getValue(), out, formatter);
+    }
+    out.write("</tbody>");
+    out.write("</table>");
+    out.write("<p></p>");
+  }
+
+  private static void emitMeasureTableHeader(PrintWriter out, Formatter formatter) {
+    out.write("<thead>");
+    out.write("<tr>");
+    formatter.format("<th colspan=1>%s</th>", TABLE_HEADER_MEASURE);
+    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_DESCRIPTION);
+    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_UNIT);
+    formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_MEASURE_TYPE);
+    out.write("</tr>");
+    out.write("</thead>");
+  }
+
+  private static void emitMeasureTableRow(Measure measure, PrintWriter out, Formatter formatter) {
+    out.write("<tr>");
+    formatter.format("<td><b>%s</b></td>", measure.getName());
+    formatter.format("<td class=\"borderLL\">%s&nbsp;</td>", measure.getDescription());
+    formatter.format("<td class=\"borderLL\">%s&nbsp;</td>", measure.getUnit());
+    String measureType =
+        measure.match(
+            Functions.returnConstant("Double"),
+            Functions.returnConstant("Long"),
+            Functions.throwAssertionError());
+    formatter.format("<td class=\"borderLL\">%s&nbsp;</td>", measureType);
+    out.write("</tr>");
+  }
+
+  static StatszZPageHandler create(ViewManager viewManager) {
+    return new StatszZPageHandler(viewManager);
+  }
+
+  private StatszZPageHandler(ViewManager viewManager) {
+    this.viewManager = viewManager;
+  }
+
+  /*
+   * TreeNode for storing the structure of views and directories that they're in. Think of this as
+   * file descriptors for view: non-leaf nodes are directories which may contain views or other
+   * directories, and leaf nodes are the ones with actual information on views. Each non-leaf node
+   * also has the number of views under its directory.
+   */
+  private static class TreeNode {
+    // Only leaf nodes have views.
+    @javax.annotation.Nullable final View.Name viewName;
+
+    // A mapping from relative path to children TreeNodes. Sorted by the relative path.
+    SortedMap<String, TreeNode> children = Maps.newTreeMap();
+
+    // The number of views that a directory contains. 0 for leaf node.
+    int views = 0;
+
+    TreeNode() {
+      this.viewName = null;
+    }
+
+    TreeNode(View.Name viewName) {
+      this.viewName = checkNotNull(viewName, "view name");
+    }
+  }
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/Style.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/Style.java
new file mode 100644
index 0000000..015b83d
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/Style.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018, 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.zpages;
+
+final class Style {
+  private Style() {}
+
+  static String style =
+      "body{font-family: 'Roboto',sans-serif;"
+          + "font-size: 14px;background-color: #F2F4EC;}"
+          + "h1{color: #3D3D3D;text-align: center;margin-bottom: 20px;}"
+          + "p{padding: 0 0.5em;color: #3D3D3D;}"
+          + "h2{color: #3D3D3D;font-size: 1.5em;background-color: #FFF;"
+          + "line-height: 2.0;margin-bottom: 0;padding: 0 0.5em;}"
+          + "h3{font-size:16px;padding:0 0.5em;margin-top:6px;margin-bottom:25px;}"
+          + "a{color:#A94442;}"
+          + "p.header{font-family: 'Open Sans', sans-serif;top: 0;left: 0;width: 100%;"
+          + "height: 60px;vertical-align: middle;color: #C1272D;font-size: 22pt;}"
+          + "p.view{font-size: 20px;margin-bottom: 0;}"
+          + ".header span{color: #3D3D3D;}"
+          + "img.oc{vertical-align: middle;}"
+          + "table{width: 100%;color: #FFF;background-color: #FFF;overflow: hidden;"
+          + "margin-bottom: 30px;margin-top: 0;border-bottom: 1px solid #3D3D3D;"
+          + "border-left: 1px solid #3D3D3D;border-right: 1px solid #3D3D3D;}"
+          + "table.title{width:100%;color:#3D3D3D;background-color:#FFF;"
+          + "border:none;line-height:2.0;margin-bottom:0;}"
+          + "thead{color: #FFF;background-color: #A94442;"
+          + "line-height:3.0;padding:0 0.5em;}"
+          + "th{color: #FFF;background-color: #A94442;"
+          + "line-height:3.0;padding:0 0.5em;}"
+          + "th.borderL{border-left:1px solid #FFF; text-align:left;}"
+          + "th.borderRL{border-right:1px solid #FFF; text-align:left;}"
+          + "th.borderLB{border-left:1px solid #FFF;"
+          + "border-bottom:1px solid #FFF;margin:0 10px;}"
+          + "tr.direct{font-size:16px;padding:0 0.5em;background-color:#F2F4EC;}"
+          + "tr:nth-child(even){background-color: #F2F2F2;}"
+          + "td{color: #3D3D3D;line-height: 2.0;text-align: left;padding: 0 0.5em;}"
+          + "td.borderLC{border-left:1px solid #3D3D3D;text-align:center;}"
+          + "td.borderLL{border-left:1px solid #3D3D3D;text-align:left;}"
+          + "td.borderRL{border-right:1px solid #3D3D3D;text-align:left;}"
+          + "td.borderRW{border-right:1px solid #FFF}"
+          + "td.borderLW{border-left:1px solid #FFF;}"
+          + "td.centerW{text-align:center;color:#FFF;}"
+          + "td.center{text-align:center;color:#3D3D3D;}"
+          + "tr.bgcolor{background-color:#A94442;}"
+          + "h1.left{text-align:left;margin-left:20px;}"
+          + "table.small{width:40%;background-color:#FFF;"
+          + "margin-left:20px;margin-bottom:30px;}"
+          + "table.small{width:40%;background-color:#FFF;"
+          + "margin-left:20px;margin-bottom:30px;}"
+          + "td.col_headR{background-color:#A94442;"
+          + "line-height:3.0;color:#FFF;border-right:1px solid #FFF;}"
+          + "td.col_head{background-color:#A94442;"
+          + "line-height:3.0;color:#FFF;}"
+          + "b.title{margin-left:20px;font-weight:bold;line-height:2.0;}"
+          + "input.button{margin-left:20px;margin-top:4px;"
+          + "font-size:20px;width:80px;height:60px;}"
+          + "td.head{text-align:center;color:#FFF;line-height:3.0;}";
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TraceConfigzZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TraceConfigzZPageHandler.java
new file mode 100644
index 0000000..2a02cca
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TraceConfigzZPageHandler.java
@@ -0,0 +1,223 @@
+/*
+ * 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.zpages;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import com.google.common.base.Charsets;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.Map;
+
+// TODO(bdrutu): Add tests.
+// TODO(hailongwen): Remove the usage of `NetworkEvent` in the future.
+/**
+ * HTML page formatter for tracing config. The page displays information about the current active
+ * tracing configuration and allows users to change it.
+ */
+final class TraceConfigzZPageHandler extends ZPageHandler {
+  private static final String TRACE_CONFIGZ_URL = "/traceconfigz";
+  private final TraceConfig traceConfig;
+
+  private static final String CHANGE = "change";
+  private static final String PERMANENT_CHANGE = "permanently";
+  private static final String RESTORE_DEFAULT_CHANGE = "restore_default";
+  private static final String QUERY_COMPONENT_SAMPLING_PROBABILITY = "samplingprobability";
+  private static final String QUERY_COMPONENT_MAX_NUMBER_OF_ATTRIBUTES = "maxnumberofattributes";
+  private static final String QUERY_COMPONENT_MAX_NUMBER_OF_ANNOTATIONS = "maxnumberofannotations";
+  private static final String QUERY_COMPONENT_MAX_NUMBER_OF_NETWORK_EVENTS =
+      "maxnumberofnetworkevents";
+  private static final String QUERY_COMPONENT_MAX_NUMBER_OF_LINKS = "maxnumberoflinks";
+
+  // TODO(bdrutu): Use post.
+  // TODO(bdrutu): Refactor this to not use a big "printf".
+  private static final String TRACECONFIGZ_FORM_BODY =
+      "<form action=/traceconfigz method=get>%n"
+          // Permanently changes table.
+          + "<table class=\"small\" rules=\"all\">%n"
+          + "<td colspan=\"3\" class=\"col_head\">Permanently change "
+          + "<input type=\"hidden\" name=\"%s\" value=\"%s\"></td>%n"
+          + "<tr><td>SamplingProbability to</td> "
+          + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%s)</td>%n"
+          + "<tr><td>MaxNumberOfAttributes to</td> "
+          + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%d)</td>%n"
+          + "<tr><td>MaxNumberOfAnnotations to</td>"
+          + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%d)</td>%n"
+          + "<tr><td>MaxNumberOfNetworkEvents to</td> "
+          + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%d)</td>%n"
+          + "<tr><td>MaxNumberOfLinks to</td>"
+          + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%d)</td>%n"
+          + "</table>%n"
+          // Submit button.
+          + "<input class=\"button\" type=submit value=Submit>%n"
+          + "</form>";
+
+  private static final String RESTORE_DEFAULT_FORM_BODY =
+      "<form action=/traceconfigz method=get>%n"
+          // Restore to default.
+          + "<b class=\"title\">Restore default</b> %n"
+          + "<input type=\"hidden\" name=\"%s\" value=\"%s\"></td>%n"
+          + "</br>%n"
+          // Reset button.
+          + "<input class=\"button\" type=submit value=Reset>%n"
+          + "</form>";
+
+  static TraceConfigzZPageHandler create(TraceConfig traceConfig) {
+    return new TraceConfigzZPageHandler(traceConfig);
+  }
+
+  @Override
+  public String getUrlPath() {
+    return TRACE_CONFIGZ_URL;
+  }
+
+  private static void emitStyle(PrintWriter out) {
+    out.write("<style>\n");
+    out.write(Style.style);
+    out.write("</style>\n");
+  }
+
+  @Override
+  @SuppressWarnings("deprecation")
+  public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
+    PrintWriter out =
+        new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
+    out.write("<!DOCTYPE html>\n");
+    out.write("<html lang=\"en\"><head>\n");
+    out.write("<meta charset=\"utf-8\">\n");
+    out.write("<title>TraceConfigZ</title>\n");
+    out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
+    out.write(
+        "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+            + "rel=\"stylesheet\">\n");
+    out.write(
+        "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
+    emitStyle(out);
+    out.write("</head>\n");
+    out.write("<body>\n");
+    out.write(
+        "<p class=\"header\">"
+            + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
+            + "Open<span>Census</span></p>");
+    out.write("<h1 class=\"left\">Trace Configuration</h1>");
+    out.write("<p></p>");
+    try {
+      // Work that can throw exceptions.
+      maybeApplyChanges(queryMap);
+    } finally {
+      // TODO(bdrutu): Maybe display to the page if an exception happened.
+      // Display the page in any case.
+      out.printf(
+          TRACECONFIGZ_FORM_BODY,
+          CHANGE,
+          PERMANENT_CHANGE,
+          QUERY_COMPONENT_SAMPLING_PROBABILITY,
+          "0.0001", // TODO(bdrutu): Get this from the default sampler (if possible).
+          QUERY_COMPONENT_MAX_NUMBER_OF_ATTRIBUTES,
+          TraceParams.DEFAULT.getMaxNumberOfAttributes(),
+          QUERY_COMPONENT_MAX_NUMBER_OF_ANNOTATIONS,
+          TraceParams.DEFAULT.getMaxNumberOfAnnotations(),
+          QUERY_COMPONENT_MAX_NUMBER_OF_NETWORK_EVENTS,
+          TraceParams.DEFAULT.getMaxNumberOfNetworkEvents(),
+          QUERY_COMPONENT_MAX_NUMBER_OF_LINKS,
+          TraceParams.DEFAULT.getMaxNumberOfLinks());
+      out.write("<br>\n");
+      out.printf(RESTORE_DEFAULT_FORM_BODY, CHANGE, RESTORE_DEFAULT_CHANGE);
+      out.write("<br>\n");
+      emitTraceParamsTable(traceConfig.getActiveTraceParams(), out);
+      out.write("</body>\n");
+      out.write("</html>\n");
+      out.close();
+    }
+  }
+
+  // If this is a supported change (currently only permanent changes are supported) apply it.
+  @SuppressWarnings("deprecation")
+  private void maybeApplyChanges(Map<String, String> queryMap) {
+    String changeStr = queryMap.get(CHANGE);
+    if (PERMANENT_CHANGE.equals(changeStr)) {
+      TraceParams.Builder traceParamsBuilder = traceConfig.getActiveTraceParams().toBuilder();
+      String samplingProbabilityStr = queryMap.get(QUERY_COMPONENT_SAMPLING_PROBABILITY);
+      if (!isNullOrEmpty(samplingProbabilityStr)) {
+        double samplingProbability = Double.parseDouble(samplingProbabilityStr);
+        traceParamsBuilder.setSampler(Samplers.probabilitySampler(samplingProbability));
+      }
+      String maxNumberOfAttributesStr = queryMap.get(QUERY_COMPONENT_MAX_NUMBER_OF_ATTRIBUTES);
+      if (!isNullOrEmpty(maxNumberOfAttributesStr)) {
+        int maxNumberOfAttributes = Integer.parseInt(maxNumberOfAttributesStr);
+        traceParamsBuilder.setMaxNumberOfAttributes(maxNumberOfAttributes);
+      }
+      String maxNumberOfAnnotationsStr = queryMap.get(QUERY_COMPONENT_MAX_NUMBER_OF_ANNOTATIONS);
+      if (!isNullOrEmpty(maxNumberOfAnnotationsStr)) {
+        int maxNumberOfAnnotations = Integer.parseInt(maxNumberOfAnnotationsStr);
+        traceParamsBuilder.setMaxNumberOfAnnotations(maxNumberOfAnnotations);
+      }
+      String maxNumberOfNetworkEventsStr =
+          queryMap.get(QUERY_COMPONENT_MAX_NUMBER_OF_NETWORK_EVENTS);
+      if (!isNullOrEmpty(maxNumberOfNetworkEventsStr)) {
+        int maxNumberOfNetworkEvents = Integer.parseInt(maxNumberOfNetworkEventsStr);
+        traceParamsBuilder.setMaxNumberOfNetworkEvents(maxNumberOfNetworkEvents);
+      }
+      String maxNumverOfLinksStr = queryMap.get(QUERY_COMPONENT_MAX_NUMBER_OF_LINKS);
+      if (!isNullOrEmpty(maxNumverOfLinksStr)) {
+        int maxNumberOfLinks = Integer.parseInt(maxNumverOfLinksStr);
+        traceParamsBuilder.setMaxNumberOfLinks(maxNumberOfLinks);
+      }
+      traceConfig.updateActiveTraceParams(traceParamsBuilder.build());
+    } else if (RESTORE_DEFAULT_CHANGE.equals(changeStr)) {
+      traceConfig.updateActiveTraceParams(TraceParams.DEFAULT);
+    }
+  }
+
+  // Prints a table to a PrintWriter that shows existing trace parameters.
+  @SuppressWarnings("deprecation")
+  private static void emitTraceParamsTable(TraceParams params, PrintWriter out) {
+    out.write(
+        "<b class=\"title\">Active tracing parameters:</b><br>\n"
+            + "<table class=\"small\" rules=\"all\">\n"
+            + "  <tr>\n"
+            + "    <td class=\"col_headR\">Name</td>\n"
+            + "    <td class=\"col_head\">Value</td>\n"
+            + "  </tr>\n");
+    out.printf(
+        "  <tr>%n    <td>Sampler</td>%n    <td>%s</td>%n  </tr>%n",
+        params.getSampler().getDescription());
+    out.printf(
+        "  <tr>%n    <td>MaxNumberOfAttributes</td>%n    <td>%d</td>%n  </tr>%n",
+        params.getMaxNumberOfAttributes());
+    out.printf(
+        "  <tr>%n    <td>MaxNumberOfAnnotations</td>%n    <td>%d</td>%n  </tr>%n",
+        params.getMaxNumberOfAnnotations());
+    out.printf(
+        "  <tr>%n    <td>MaxNumberOfNetworkEvents</td>%n    <td>%d</td>%n  </tr>%n",
+        params.getMaxNumberOfNetworkEvents());
+    out.printf(
+        "  <tr>%n    <td>MaxNumberOfLinks</td>%n    <td>%d</td>%n  </tr>%n",
+        params.getMaxNumberOfLinks());
+
+    out.write("</table>\n");
+  }
+
+  private TraceConfigzZPageHandler(TraceConfig traceConfig) {
+    this.traceConfig = traceConfig;
+  }
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TracezZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TracezZPageHandler.java
new file mode 100644
index 0000000..f6a3699
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TracezZPageHandler.java
@@ -0,0 +1,699 @@
+/*
+ * 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.zpages;
+
+import static com.google.common.html.HtmlEscapers.htmlEscaper;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SampledSpanStore;
+import io.opencensus.trace.export.SampledSpanStore.ErrorFilter;
+import io.opencensus.trace.export.SampledSpanStore.LatencyBucketBoundaries;
+import io.opencensus.trace.export.SampledSpanStore.LatencyFilter;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+// TODO(hailongwen): remove the usage of `NetworkEvent` in the future.
+/**
+ * HTML page formatter for tracing debug. The page displays information about all active spans and
+ * all sampled spans based on latency and errors.
+ *
+ * <p>It prints a summary table which contains one row for each span name and data about number of
+ * active and sampled spans.
+ */
+final class TracezZPageHandler extends ZPageHandler {
+  private enum RequestType {
+    RUNNING(0),
+    FINISHED(1),
+    FAILED(2),
+    UNKNOWN(-1);
+
+    private final int value;
+
+    RequestType(int value) {
+      this.value = value;
+    }
+
+    static RequestType fromString(String str) {
+      int value = Integer.parseInt(str);
+      switch (value) {
+        case 0:
+          return RUNNING;
+        case 1:
+          return FINISHED;
+        case 2:
+          return FAILED;
+        default:
+          return UNKNOWN;
+      }
+    }
+
+    int getValue() {
+      return value;
+    }
+  }
+
+  private static final String TRACEZ_URL = "/tracez";
+  private static final Tracer tracer = Tracing.getTracer();
+  // Color to use for zebra-striping.
+  private static final String ZEBRA_STRIPE_COLOR = "#FFF";
+  // Color for sampled traceIds.
+  private static final String SAMPLED_TRACE_ID_COLOR = "#C1272D";
+  // Color for not sampled traceIds
+  private static final String NOT_SAMPLED_TRACE_ID_COLOR = "black";
+  // The header for span name.
+  private static final String HEADER_SPAN_NAME = "zspanname";
+  // The header for type (running = 0, latency = 1, error = 2) to display.
+  private static final String HEADER_SAMPLES_TYPE = "ztype";
+  // The header for sub-type:
+  // * for latency based samples [0, 8] representing the latency buckets, where 0 is the first one;
+  // * for error based samples [0, 15], 0 - means all, otherwise the error code;
+  private static final String HEADER_SAMPLES_SUB_TYPE = "zsubtype";
+  // Map from LatencyBucketBoundaries to the human string displayed on the UI for each bucket.
+  private static final Map<LatencyBucketBoundaries, String> LATENCY_BUCKET_BOUNDARIES_STRING_MAP =
+      buildLatencyBucketBoundariesStringMap();
+  @javax.annotation.Nullable private final RunningSpanStore runningSpanStore;
+  @javax.annotation.Nullable private final SampledSpanStore sampledSpanStore;
+
+  private TracezZPageHandler(
+      @javax.annotation.Nullable RunningSpanStore runningSpanStore,
+      @javax.annotation.Nullable SampledSpanStore sampledSpanStore) {
+    this.runningSpanStore = runningSpanStore;
+    this.sampledSpanStore = sampledSpanStore;
+  }
+
+  /**
+   * Constructs a new {@code TracezZPageHandler}.
+   *
+   * @param runningSpanStore the instance of the {@code RunningSpanStore} to be used.
+   * @param sampledSpanStore the instance of the {@code SampledSpanStore} to be used.
+   * @return a new {@code TracezZPageHandler}.
+   */
+  static TracezZPageHandler create(
+      @javax.annotation.Nullable RunningSpanStore runningSpanStore,
+      @javax.annotation.Nullable SampledSpanStore sampledSpanStore) {
+    return new TracezZPageHandler(runningSpanStore, sampledSpanStore);
+  }
+
+  @Override
+  public String getUrlPath() {
+    return TRACEZ_URL;
+  }
+
+  private static void emitStyle(PrintWriter out) {
+    out.write("<style>\n");
+    out.write(Style.style);
+    out.write("</style>\n");
+  }
+
+  @Override
+  public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
+    PrintWriter out =
+        new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
+    out.write("<!DOCTYPE html>\n");
+    out.write("<html lang=\"en\"><head>\n");
+    out.write("<meta charset=\"utf-8\">\n");
+    out.write("<title>TraceZ</title>\n");
+    out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
+    out.write(
+        "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+            + "rel=\"stylesheet\">\n");
+    out.write(
+        "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
+    emitStyle(out);
+    out.write("</head>\n");
+    out.write("<body>\n");
+    out.write(
+        "<p class=\"header\">"
+            + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
+            + "Open<span>Census</span></p>");
+    out.write("<h1>TraceZ Summary</h1>\n");
+
+    try {
+      emitHtmlBody(queryMap, out);
+    } catch (Throwable t) {
+      out.write("Errors while generate the HTML page " + t);
+    }
+    out.write("</body>\n");
+    out.write("</html>\n");
+    out.close();
+  }
+
+  private void emitHtmlBody(Map<String, String> queryMap, PrintWriter out)
+      throws UnsupportedEncodingException {
+    if (runningSpanStore == null || sampledSpanStore == null) {
+      out.write("OpenCensus implementation not available.");
+      return;
+    }
+    Formatter formatter = new Formatter(out, Locale.US);
+    emitSummaryTable(out, formatter);
+    String spanName = queryMap.get(HEADER_SPAN_NAME);
+    if (spanName != null) {
+      tracer
+          .getCurrentSpan()
+          .addAnnotation(
+              "Render spans.",
+              ImmutableMap.<String, AttributeValue>builder()
+                  .put("SpanName", AttributeValue.stringAttributeValue(spanName))
+                  .build());
+      String typeStr = queryMap.get(HEADER_SAMPLES_TYPE);
+      if (typeStr != null) {
+        List<SpanData> spans = null;
+        RequestType type = RequestType.fromString(typeStr);
+        if (type == RequestType.UNKNOWN) {
+          return;
+        }
+        if (type == RequestType.RUNNING) {
+          // Display running.
+          spans =
+              new ArrayList<>(
+                  runningSpanStore.getRunningSpans(RunningSpanStore.Filter.create(spanName, 0)));
+          // Sort active spans incremental.
+          Collections.sort(spans, new SpanDataComparator(true));
+        } else {
+          String subtypeStr = queryMap.get(HEADER_SAMPLES_SUB_TYPE);
+          if (subtypeStr != null) {
+            int subtype = Integer.parseInt(subtypeStr);
+            if (type == RequestType.FAILED) {
+              if (subtype < 0 || subtype >= CanonicalCode.values().length) {
+                return;
+              }
+              // Display errors. subtype 0 means all.
+              CanonicalCode code = subtype == 0 ? null : CanonicalCode.values()[subtype];
+              spans =
+                  new ArrayList<>(
+                      sampledSpanStore.getErrorSampledSpans(ErrorFilter.create(spanName, code, 0)));
+            } else {
+              if (subtype < 0 || subtype >= LatencyBucketBoundaries.values().length) {
+                return;
+              }
+              // Display latency.
+              LatencyBucketBoundaries latencyBucketBoundaries =
+                  LatencyBucketBoundaries.values()[subtype];
+              spans =
+                  new ArrayList<>(
+                      sampledSpanStore.getLatencySampledSpans(
+                          LatencyFilter.create(
+                              spanName,
+                              latencyBucketBoundaries.getLatencyLowerNs(),
+                              latencyBucketBoundaries.getLatencyUpperNs(),
+                              0)));
+              // Sort sampled spans decremental.
+              Collections.sort(spans, new SpanDataComparator(false));
+            }
+          }
+        }
+        emitSpanNameAndCountPages(formatter, spanName, spans == null ? 0 : spans.size(), type);
+
+        if (spans != null) {
+          emitSpans(out, formatter, spans);
+          emitLegend(out);
+        }
+      }
+    }
+  }
+
+  private static void emitSpanNameAndCountPages(
+      Formatter formatter, String spanName, int returnedNum, RequestType type) {
+    formatter.format("<p><b>Span Name: %s </b></p>%n", htmlEscaper().escape(spanName));
+    formatter.format(
+        "%s Requests %d</b></p>%n",
+        type == RequestType.RUNNING
+            ? "Running"
+            : type == RequestType.FINISHED ? "Finished" : "Failed",
+        returnedNum);
+  }
+
+  /** Emits the list of SampledRequets with a header. */
+  private static void emitSpans(PrintWriter out, Formatter formatter, Collection<SpanData> spans) {
+    out.write("<pre>\n");
+    formatter.format("%-23s %18s%n", "When", "Elapsed(s)");
+    out.write("-------------------------------------------\n");
+    for (SpanData span : spans) {
+      tracer
+          .getCurrentSpan()
+          .addAnnotation(
+              "Render span.",
+              ImmutableMap.<String, AttributeValue>builder()
+                  .put(
+                      "SpanId",
+                      AttributeValue.stringAttributeValue(
+                          BaseEncoding.base16()
+                              .lowerCase()
+                              .encode(span.getContext().getSpanId().getBytes())))
+                  .build());
+
+      emitSingleSpan(out, formatter, span);
+    }
+    out.write("</pre>\n");
+  }
+
+  // Emits the internal html for a single {@link SpanData}.
+  @SuppressWarnings("deprecation")
+  private static void emitSingleSpan(PrintWriter out, Formatter formatter, SpanData span) {
+    Calendar calendar = Calendar.getInstance();
+    calendar.setTimeInMillis(TimeUnit.SECONDS.toMillis(span.getStartTimestamp().getSeconds()));
+    long microsField = TimeUnit.NANOSECONDS.toMicros(span.getStartTimestamp().getNanos());
+    String elapsedSecondsStr =
+        span.getEndTimestamp() != null
+            ? String.format(
+                "%13.6f",
+                durationToNanos(span.getEndTimestamp().subtractTimestamp(span.getStartTimestamp()))
+                    * 1.0e-9)
+            : String.format("%13s", " ");
+
+    SpanContext spanContext = span.getContext();
+    formatter.format(
+        "<b>%04d/%02d/%02d-%02d:%02d:%02d.%06d %s     TraceId: <b style=\"color:%s;\">%s</b> "
+            + "SpanId: %s ParentSpanId: %s</b>%n",
+        calendar.get(Calendar.YEAR),
+        calendar.get(Calendar.MONTH) + 1,
+        calendar.get(Calendar.DAY_OF_MONTH),
+        calendar.get(Calendar.HOUR_OF_DAY),
+        calendar.get(Calendar.MINUTE),
+        calendar.get(Calendar.SECOND),
+        microsField,
+        elapsedSecondsStr,
+        spanContext.getTraceOptions().isSampled()
+            ? SAMPLED_TRACE_ID_COLOR
+            : NOT_SAMPLED_TRACE_ID_COLOR,
+        BaseEncoding.base16().lowerCase().encode(spanContext.getTraceId().getBytes()),
+        BaseEncoding.base16().lowerCase().encode(spanContext.getSpanId().getBytes()),
+        BaseEncoding.base16()
+            .lowerCase()
+            .encode(
+                span.getParentSpanId() == null
+                    ? SpanId.INVALID.getBytes()
+                    : span.getParentSpanId().getBytes()));
+
+    int lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
+
+    Timestamp lastTimestampNanos = span.getStartTimestamp();
+    TimedEvents<Annotation> annotations = span.getAnnotations();
+    TimedEvents<io.opencensus.trace.NetworkEvent> networkEvents = span.getNetworkEvents();
+    List<TimedEvent<?>> timedEvents = new ArrayList<TimedEvent<?>>(annotations.getEvents());
+    timedEvents.addAll(networkEvents.getEvents());
+    Collections.sort(timedEvents, new TimedEventComparator());
+    for (TimedEvent<?> event : timedEvents) {
+      // Special printing so that durations smaller than one second
+      // are left padded with blanks instead of '0' characters.
+      // E.g.,
+      //        Number                  Printout
+      //        ---------------------------------
+      //        0.000534                  .   534
+      //        1.000534                 1.000534
+      long deltaMicros =
+          TimeUnit.NANOSECONDS.toMicros(
+              durationToNanos(event.getTimestamp().subtractTimestamp(lastTimestampNanos)));
+      String deltaString;
+      if (deltaMicros >= 1000000) {
+        deltaString = String.format("%.6f", (deltaMicros / 1000000.0));
+      } else {
+        deltaString = String.format(".%6d", deltaMicros);
+      }
+
+      calendar.setTimeInMillis(
+          TimeUnit.SECONDS.toMillis(event.getTimestamp().getSeconds())
+              + TimeUnit.NANOSECONDS.toMillis(event.getTimestamp().getNanos()));
+      microsField = TimeUnit.NANOSECONDS.toMicros(event.getTimestamp().getNanos());
+
+      int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
+      if (dayOfYear == lastEntryDayOfYear) {
+        formatter.format("%11s", "");
+      } else {
+        formatter.format(
+            "%04d/%02d/%02d-",
+            calendar.get(Calendar.YEAR),
+            calendar.get(Calendar.MONTH) + 1,
+            calendar.get(Calendar.DAY_OF_MONTH));
+        lastEntryDayOfYear = dayOfYear;
+      }
+
+      formatter.format(
+          "%02d:%02d:%02d.%06d %13s ... %s%n",
+          calendar.get(Calendar.HOUR_OF_DAY),
+          calendar.get(Calendar.MINUTE),
+          calendar.get(Calendar.SECOND),
+          microsField,
+          deltaString,
+          htmlEscaper()
+              .escape(
+                  event.getEvent() instanceof Annotation
+                      ? renderAnnotation((Annotation) event.getEvent())
+                      : renderNetworkEvents(
+                          (io.opencensus.trace.NetworkEvent) castNonNull(event.getEvent()))));
+      lastTimestampNanos = event.getTimestamp();
+    }
+    Status status = span.getStatus();
+    if (status != null) {
+      formatter.format("%44s %s%n", "", htmlEscaper().escape(renderStatus(status)));
+    }
+    formatter.format(
+        "%44s %s%n",
+        "", htmlEscaper().escape(renderAttributes(span.getAttributes().getAttributeMap())));
+  }
+
+  // TODO(sebright): Remove this method.
+  @SuppressWarnings("nullness")
+  private static <T> T castNonNull(@javax.annotation.Nullable T arg) {
+    return arg;
+  }
+
+  // Emits the summary table with links to all samples.
+  private void emitSummaryTable(PrintWriter out, Formatter formatter)
+      throws UnsupportedEncodingException {
+    if (runningSpanStore == null || sampledSpanStore == null) {
+      return;
+    }
+    RunningSpanStore.Summary runningSpanStoreSummary = runningSpanStore.getSummary();
+    SampledSpanStore.Summary sampledSpanStoreSummary = sampledSpanStore.getSummary();
+
+    out.write("<table style='border-spacing: 0;\n");
+    out.write("border-left:1px solid #3D3D3D;border-right:1px solid #3D3D3D;'>\n");
+    emitSummaryTableHeader(out, formatter);
+
+    Set<String> spanNames = new TreeSet<>(runningSpanStoreSummary.getPerSpanNameSummary().keySet());
+    spanNames.addAll(sampledSpanStoreSummary.getPerSpanNameSummary().keySet());
+    boolean zebraColor = true;
+    for (String spanName : spanNames) {
+      out.write("<tr class=\"border\">\n");
+      if (!zebraColor) {
+        out.write("<tr class=\"border\">\n");
+      } else {
+        formatter.format("<tr class=\"border\" style=\"background: %s\">%n", ZEBRA_STRIPE_COLOR);
+      }
+      zebraColor = !zebraColor;
+      formatter.format("<td>%s</td>%n", htmlEscaper().escape(spanName));
+
+      // Running
+      out.write("<td class=\"borderRL\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+      RunningSpanStore.PerSpanNameSummary runningSpanStorePerSpanNameSummary =
+          runningSpanStoreSummary.getPerSpanNameSummary().get(spanName);
+
+      // subtype ignored for running requests.
+      emitSingleCell(
+          out,
+          formatter,
+          spanName,
+          runningSpanStorePerSpanNameSummary == null
+              ? 0
+              : runningSpanStorePerSpanNameSummary.getNumRunningSpans(),
+          RequestType.RUNNING,
+          0);
+
+      SampledSpanStore.PerSpanNameSummary sampledSpanStorePerSpanNameSummary =
+          sampledSpanStoreSummary.getPerSpanNameSummary().get(spanName);
+
+      // Latency based samples
+      out.write("<td class=\"borderLC\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+      Map<LatencyBucketBoundaries, Integer> latencyBucketsSummaries =
+          sampledSpanStorePerSpanNameSummary != null
+              ? sampledSpanStorePerSpanNameSummary.getNumbersOfLatencySampledSpans()
+              : null;
+      int subtype = 0;
+      for (LatencyBucketBoundaries latencyBucketsBoundaries : LatencyBucketBoundaries.values()) {
+        if (latencyBucketsSummaries != null) {
+          int numSamples =
+              latencyBucketsSummaries.containsKey(latencyBucketsBoundaries)
+                  ? latencyBucketsSummaries.get(latencyBucketsBoundaries)
+                  : 0;
+          emitSingleCell(out, formatter, spanName, numSamples, RequestType.FINISHED, subtype++);
+        } else {
+          // numSamples < -1 means "Not Available".
+          emitSingleCell(out, formatter, spanName, -1, RequestType.FINISHED, subtype++);
+        }
+      }
+
+      // Error based samples.
+      out.write("<td class=\"borderRL\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+      if (sampledSpanStorePerSpanNameSummary != null) {
+        Map<CanonicalCode, Integer> errorBucketsSummaries =
+            sampledSpanStorePerSpanNameSummary.getNumbersOfErrorSampledSpans();
+        int numErrorSamples = 0;
+        for (Map.Entry<CanonicalCode, Integer> it : errorBucketsSummaries.entrySet()) {
+          numErrorSamples += it.getValue();
+        }
+        // subtype 0 means all;
+        emitSingleCell(out, formatter, spanName, numErrorSamples, RequestType.FAILED, 0);
+      } else {
+        // numSamples < -1 means "Not Available".
+        emitSingleCell(out, formatter, spanName, -1, RequestType.FAILED, 0);
+      }
+
+      out.write("</tr>\n");
+    }
+    out.write("</table>");
+  }
+
+  private static void emitSummaryTableHeader(PrintWriter out, Formatter formatter) {
+    // First line.
+    out.write("<tr class=\"bgcolor\">\n");
+    out.write("<td colspan=1 class=\"head\"><b>Span Name</b></td>\n");
+    out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+    out.write("<td colspan=1 class=\"head\"><b>Running</b></td>\n");
+    out.write("<td class=\"borderLW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+    out.write("<td colspan=9 class=\"head\"><b>Latency Samples</b></td>\n");
+    out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+    out.write("<td colspan=1 class=\"head\"><b>Error Samples</b></td>\n");
+    out.write("</tr>\n");
+    // Second line.
+    out.write("<tr class=\"bgcolor\">\n");
+    out.write("<td colspan=1></td>\n");
+    out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+    out.write("<td colspan=1></td>\n");
+    out.write("<td class=\"borderLW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+    for (LatencyBucketBoundaries latencyBucketsBoundaries : LatencyBucketBoundaries.values()) {
+      formatter.format(
+          "<td colspan=1 class=\"centerW\"><b>[%s]</b></td>%n",
+          LATENCY_BUCKET_BOUNDARIES_STRING_MAP.get(latencyBucketsBoundaries));
+    }
+    out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+    out.write("<td colspan=1></td>\n");
+    out.write("</tr>\n");
+  }
+
+  // If numSamples is greater than 0 then emit a link to see span data, if the numSamples is
+  // negative then print "N/A", otherwise print the text "0".
+  private static void emitSingleCell(
+      PrintWriter out,
+      Formatter formatter,
+      String spanName,
+      int numSamples,
+      RequestType type,
+      int subtype)
+      throws UnsupportedEncodingException {
+    if (numSamples > 0) {
+      formatter.format(
+          "<td class=\"center\"><a href='?%s=%s&%s=%d&%s=%d'>%d</a></td>%n",
+          HEADER_SPAN_NAME,
+          URLEncoder.encode(spanName, "UTF-8"),
+          HEADER_SAMPLES_TYPE,
+          type.getValue(),
+          HEADER_SAMPLES_SUB_TYPE,
+          subtype,
+          numSamples);
+    } else if (numSamples < 0) {
+      out.write("<td class=\"center\">N/A</td>\n");
+    } else {
+      out.write("<td class=\"center\">0</td>\n");
+    }
+  }
+
+  private static void emitLegend(PrintWriter out) {
+    out.write("<br>\n");
+    out.printf(
+        "<p><b style=\"color:%s;\">TraceId</b> means sampled request. "
+            + "<b style=\"color:%s;\">TraceId</b> means not sampled request.</p>%n",
+        SAMPLED_TRACE_ID_COLOR, NOT_SAMPLED_TRACE_ID_COLOR);
+  }
+
+  private static Map<LatencyBucketBoundaries, String> buildLatencyBucketBoundariesStringMap() {
+    Map<LatencyBucketBoundaries, String> ret = new HashMap<>();
+    for (LatencyBucketBoundaries latencyBucketBoundaries : LatencyBucketBoundaries.values()) {
+      ret.put(latencyBucketBoundaries, latencyBucketBoundariesToString(latencyBucketBoundaries));
+    }
+    return Collections.unmodifiableMap(ret);
+  }
+
+  private static long durationToNanos(Duration duration) {
+    return TimeUnit.SECONDS.toNanos(duration.getSeconds()) + duration.getNanos();
+  }
+
+  private static String latencyBucketBoundariesToString(
+      LatencyBucketBoundaries latencyBucketBoundaries) {
+    switch (latencyBucketBoundaries) {
+      case ZERO_MICROSx10:
+        return ">0us";
+      case MICROSx10_MICROSx100:
+        return ">10us";
+      case MICROSx100_MILLIx1:
+        return ">100us";
+      case MILLIx1_MILLIx10:
+        return ">1ms";
+      case MILLIx10_MILLIx100:
+        return ">10ms";
+      case MILLIx100_SECONDx1:
+        return ">100ms";
+      case SECONDx1_SECONDx10:
+        return ">1s";
+      case SECONDx10_SECONDx100:
+        return ">10s";
+      case SECONDx100_MAX:
+        return ">100s";
+    }
+    throw new IllegalArgumentException("No value string available for: " + latencyBucketBoundaries);
+  }
+
+  @SuppressWarnings("deprecation")
+  private static String renderNetworkEvents(io.opencensus.trace.NetworkEvent networkEvent) {
+    StringBuilder stringBuilder = new StringBuilder();
+    if (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.RECV) {
+      stringBuilder.append("Received message");
+    } else if (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.SENT) {
+      stringBuilder.append("Sent message");
+    } else {
+      stringBuilder.append("Unknown");
+    }
+    stringBuilder.append(" id=");
+    stringBuilder.append(networkEvent.getMessageId());
+    stringBuilder.append(" uncompressed_size=");
+    stringBuilder.append(networkEvent.getUncompressedMessageSize());
+    stringBuilder.append(" compressed_size=");
+    stringBuilder.append(networkEvent.getCompressedMessageSize());
+    return stringBuilder.toString();
+  }
+
+  private static String renderAnnotation(Annotation annotation) {
+    StringBuilder stringBuilder = new StringBuilder();
+    stringBuilder.append(annotation.getDescription());
+    if (!annotation.getAttributes().isEmpty()) {
+      stringBuilder.append(" ");
+      stringBuilder.append(renderAttributes(annotation.getAttributes()));
+    }
+    return stringBuilder.toString();
+  }
+
+  private static String renderStatus(Status status) {
+    return status.toString();
+  }
+
+  private static String renderAttributes(Map<String, AttributeValue> attributes) {
+    StringBuilder stringBuilder = new StringBuilder();
+    stringBuilder.append("Attributes:{");
+    boolean first = true;
+    for (Map.Entry<String, AttributeValue> entry : attributes.entrySet()) {
+      if (first) {
+        first = false;
+        stringBuilder.append(entry.getKey());
+        stringBuilder.append("=");
+        stringBuilder.append(attributeValueToString(entry.getValue()));
+      } else {
+        stringBuilder.append(", ");
+        stringBuilder.append(entry.getKey());
+        stringBuilder.append("=");
+        stringBuilder.append(attributeValueToString(entry.getValue()));
+      }
+    }
+    stringBuilder.append("}");
+    return stringBuilder.toString();
+  }
+
+  // The return type needs to be nullable when this function is used as an argument to 'match' in
+  // attributeValueToString, because 'match' doesn't allow covariant return types.
+  private static final Function<Object, /*@Nullable*/ String> returnToString =
+      Functions.returnToString();
+
+  @javax.annotation.Nullable
+  private static String attributeValueToString(AttributeValue attributeValue) {
+    return attributeValue.match(
+        returnToString,
+        returnToString,
+        returnToString,
+        returnToString,
+        Functions.</*@Nullable*/ String>returnNull());
+  }
+
+  private static final class TimedEventComparator
+      implements Comparator<TimedEvent<?>>, Serializable {
+    private static final long serialVersionUID = 0;
+
+    @Override
+    public int compare(TimedEvent<?> o1, TimedEvent<?> o2) {
+      return o1.getTimestamp().compareTo(o2.getTimestamp());
+    }
+  }
+
+  private static final class SpanDataComparator implements Comparator<SpanData>, Serializable {
+    private static final long serialVersionUID = 0;
+    private final boolean incremental;
+
+    /**
+     * Returns a new {@code SpanDataComparator}.
+     *
+     * @param incremental {@code true} if sorted incremental.
+     */
+    private SpanDataComparator(boolean incremental) {
+      this.incremental = incremental;
+    }
+
+    @Override
+    public int compare(SpanData o1, SpanData o2) {
+      return incremental
+          ? o1.getStartTimestamp().compareTo(o2.getStartTimestamp())
+          : o2.getStartTimestamp().compareTo(o1.getStartTimestamp());
+    }
+  }
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandler.java
new file mode 100644
index 0000000..172bca0
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandler.java
@@ -0,0 +1,49 @@
+/*
+ * 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.zpages;
+
+import java.io.OutputStream;
+import java.util.Map;
+
+/**
+ * Main interface for all the Z-Pages. All Z-Pages must implement this interface to allow other HTTP
+ * server implementation to support these pages.
+ *
+ * @since 0.6
+ */
+public abstract class ZPageHandler {
+
+  /**
+   * Returns the URL path that should be used to register this page.
+   *
+   * @return the URL path that should be used to register this page.
+   * @since 0.6
+   */
+  public abstract String getUrlPath();
+
+  /**
+   * Emits the HTML generated page to the {@code outputStream}.
+   *
+   * @param queryMap the query components map.
+   * @param outputStream the output {@code OutputStream}.
+   * @since 0.6
+   */
+  public abstract void emitHtml(Map<String, String> queryMap, OutputStream outputStream);
+
+  /** Package protected constructor to disallow users to extend this class. */
+  ZPageHandler() {}
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandlers.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandlers.java
new file mode 100644
index 0000000..710e9a2
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandlers.java
@@ -0,0 +1,200 @@
+/*
+ * 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.zpages;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.sun.net.httpserver.HttpServer;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.trace.Tracing;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A collection of HTML pages to display stats and trace data and allow library configuration
+ * control.
+ *
+ * <p>Example usage with private {@link HttpServer}:
+ *
+ * <pre>{@code
+ * public class Main {
+ *   public static void main(String[] args) throws Exception {
+ *     ZPageHandlers.startHttpServerAndRegisterAll(8000);
+ *     ... // do work
+ *   }
+ * }
+ * }</pre>
+ *
+ * <p>Example usage with shared {@link HttpServer}:
+ *
+ * <pre>{@code
+ * public class Main {
+ *   public static void main(String[] args) throws Exception {
+ *     HttpServer server = HttpServer.create(new InetSocketAddress(8000), 10);
+ *     ZPageHandlers.registerAllToHttpServer(server);
+ *     server.start();
+ *     ... // do work
+ *   }
+ * }
+ * }</pre>
+ *
+ * @since 0.6
+ */
+@ThreadSafe
+public final class ZPageHandlers {
+  // The HttpServer listening socket backlog (maximum number of queued incoming connections).
+  private static final int BACKLOG = 5;
+  // How many seconds to wait for the HTTP server to stop.
+  private static final int STOP_DELAY = 1;
+  private static final Logger logger = Logger.getLogger(ZPageHandler.class.getName());
+  private static final ZPageHandler tracezZPageHandler =
+      TracezZPageHandler.create(
+          Tracing.getExportComponent().getRunningSpanStore(),
+          Tracing.getExportComponent().getSampledSpanStore());
+  private static final ZPageHandler traceConfigzZPageHandler =
+      TraceConfigzZPageHandler.create(Tracing.getTraceConfig());
+  private static final ZPageHandler rpczZpageHandler =
+      RpczZPageHandler.create(Stats.getViewManager());
+  private static final ZPageHandler statszZPageHandler =
+      StatszZPageHandler.create(Stats.getViewManager());
+
+  private static final Object monitor = new Object();
+
+  @GuardedBy("monitor")
+  @Nullable
+  private static HttpServer server;
+
+  /**
+   * Returns a {@code ZPageHandler} for tracing debug. The page displays information about all
+   * active spans and all sampled spans based on latency and errors.
+   *
+   * <p>It prints a summary table which contains one row for each span name and data about number of
+   * active and sampled spans.
+   *
+   * <p>If no sampled spans based on latency and error codes are available for a given name, make
+   * sure that the span name is registered to the {@code SampledSpanStore}.
+   *
+   * @return a {@code ZPageHandler} for tracing debug.
+   * @since 0.6
+   */
+  public static ZPageHandler getTracezZPageHandler() {
+    return tracezZPageHandler;
+  }
+
+  /**
+   * Returns a {@code ZPageHandler} for tracing config. The page displays information about all
+   * active configuration and allow changing the active configuration.
+   *
+   * @return a {@code ZPageHandler} for tracing config.
+   * @since 0.6
+   */
+  public static ZPageHandler getTraceConfigzZPageHandler() {
+    return traceConfigzZPageHandler;
+  }
+
+  /**
+   * Returns a {@code ZPageHandler} for gRPC stats.
+   *
+   * <p>It prints a summary table which contains rows for each gRPC method.
+   *
+   * @return a {@code ZPageHandler} for gRPC stats.
+   * @since 0.12.0
+   */
+  public static ZPageHandler getRpczZpageHandler() {
+    return rpczZpageHandler;
+  }
+
+  /**
+   * Returns a {@code ZPageHandler} for all registered {@link View}s and {@link Measure}s.
+   *
+   * <p>Only {@code Cumulative} views are exported. {@link View}s are grouped by directories.
+   *
+   * @return a {@code ZPageHandler} for all registered {@code View}s and {@code Measure}s.
+   * @since 0.12.0
+   */
+  public static ZPageHandler getStatszZPageHandler() {
+    return statszZPageHandler;
+  }
+
+  /**
+   * Registers all pages to the given {@code HttpServer}.
+   *
+   * @param server the server that exports the tracez page.
+   * @since 0.6
+   */
+  public static void registerAllToHttpServer(HttpServer server) {
+    server.createContext(tracezZPageHandler.getUrlPath(), new ZPageHttpHandler(tracezZPageHandler));
+    server.createContext(
+        traceConfigzZPageHandler.getUrlPath(), new ZPageHttpHandler(traceConfigzZPageHandler));
+    server.createContext(rpczZpageHandler.getUrlPath(), new ZPageHttpHandler(rpczZpageHandler));
+    server.createContext(statszZPageHandler.getUrlPath(), new ZPageHttpHandler(statszZPageHandler));
+  }
+
+  /**
+   * Starts an {@code HttpServer} and registers all pages to it. When the JVM shuts down the server
+   * is stopped.
+   *
+   * <p>Users must call this function only once per process.
+   *
+   * @param port the port used to bind the {@code HttpServer}.
+   * @throws IllegalStateException if the server is already started.
+   * @throws IOException if the server cannot bind to the requested address.
+   * @since 0.6
+   */
+  public static void startHttpServerAndRegisterAll(int port) throws IOException {
+    synchronized (monitor) {
+      checkState(server == null, "The HttpServer is already started.");
+      server = HttpServer.create(new InetSocketAddress(port), BACKLOG);
+      ZPageHandlers.registerAllToHttpServer(server);
+      server.start();
+      logger.fine("HttpServer started on address " + server.getAddress().toString());
+    }
+
+    // This does not need to be mutex protected because it is guaranteed that only one thread will
+    // get ever here.
+    Runtime.getRuntime()
+        .addShutdownHook(
+            new Thread() {
+              @Override
+              public void run() {
+                // Use stderr here since the logger may have been reset by its JVM shutdown hook.
+                logger.fine("*** Shutting down gRPC server (JVM shutting down)");
+                ZPageHandlers.stop();
+                logger.fine("*** Server shut down");
+              }
+            });
+  }
+
+  private static void stop() {
+    synchronized (monitor) {
+      // This should never happen because we register the shutdown hook only if we start the server.
+      if (server == null) {
+        throw new IllegalStateException("The HttpServer is already stopped.");
+      }
+      server.stop(STOP_DELAY);
+      server = null;
+    }
+  }
+
+  private ZPageHandlers() {}
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHttpHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHttpHandler.java
new file mode 100644
index 0000000..975bdfc
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHttpHandler.java
@@ -0,0 +1,88 @@
+/*
+ * 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.zpages;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import io.opencensus.common.Scope;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** An {@link HttpHandler} that can be used to render HTML pages using any {@code ZPageHandler}. */
+final class ZPageHttpHandler implements HttpHandler {
+  private static final Tracer tracer = Tracing.getTracer();
+  private static final String HTTP_SERVER = "HttpServer";
+  private final ZPageHandler zpageHandler;
+  private final String httpServerSpanName;
+
+  /** Constructs a new {@code ZPageHttpHandler}. */
+  ZPageHttpHandler(ZPageHandler zpageHandler) {
+    this.zpageHandler = zpageHandler;
+    this.httpServerSpanName = HTTP_SERVER + zpageHandler.getUrlPath();
+    Tracing.getExportComponent()
+        .getSampledSpanStore()
+        .registerSpanNamesForCollection(Arrays.asList(httpServerSpanName));
+  }
+
+  @Override
+  public final void handle(HttpExchange httpExchange) throws IOException {
+    try (Scope ss =
+        tracer
+            .spanBuilderWithExplicitParent(httpServerSpanName, null)
+            .setRecordEvents(true)
+            .startScopedSpan()) {
+      tracer
+          .getCurrentSpan()
+          .putAttribute(
+              "/http/method ",
+              AttributeValue.stringAttributeValue(httpExchange.getRequestMethod()));
+      httpExchange.sendResponseHeaders(200, 0);
+      zpageHandler.emitHtml(
+          uriQueryToMap(httpExchange.getRequestURI()), httpExchange.getResponseBody());
+    } finally {
+      httpExchange.close();
+    }
+  }
+
+  @VisibleForTesting
+  static Map<String, String> uriQueryToMap(URI uri) {
+    String query = uri.getQuery();
+    if (query == null) {
+      return Collections.emptyMap();
+    }
+    Map<String, String> result = new HashMap<String, String>();
+    for (String param : Splitter.on("&").split(query)) {
+      List<String> splits = Splitter.on("=").splitToList(param);
+      if (splits.size() > 1) {
+        result.put(splits.get(0), splits.get(1));
+      } else {
+        result.put(splits.get(0), "");
+      }
+    }
+    return result;
+  }
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/RpczZPageHandlerTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/RpczZPageHandlerTest.java
new file mode 100644
index 0000000..2a75fe8
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/RpczZPageHandlerTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2018, 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.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW;
+import static org.mockito.Mockito.doReturn;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagValue;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link RpczZPageHandler}. */
+@RunWith(JUnit4.class)
+public class RpczZPageHandlerTest {
+
+  @Mock private final ViewManager mockViewManager = Mockito.mock(ViewManager.class);
+
+  private static final TagValue METHOD_1 = TagValue.create("method1");
+  private static final TagValue METHOD_2 = TagValue.create("method2");
+  private static final MeanData MEAN_DATA_1 = MeanData.create(5.5, 11);
+  private static final MeanData MEAN_DATA_2 = MeanData.create(1, 3);
+  private static final MeanData MEAN_DATA_3 = MeanData.create(1, 2);
+  private static final DistributionData DISTRIBUTION_DATA =
+      DistributionData.create(4.2, 5, 0.2, 16.3, 234.56, Arrays.asList(1L, 0L, 1L, 2L, 1L));
+  private static final CumulativeData CUMULATIVE_DATA =
+      CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(5000));
+  private static final IntervalData INTERVAL_DATA = IntervalData.create(Timestamp.fromMillis(8000));
+
+  @Test
+  public void getUrl() {
+    RpczZPageHandler handler = RpczZPageHandler.create(mockViewManager);
+    assertThat(handler.getUrlPath()).isEqualTo("/rpcz");
+  }
+
+  @Test
+  public void emitSummaryTableForEachMethod() {
+    doReturn(
+            ViewData.create(
+                RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW,
+                ImmutableMap.of(Arrays.asList(METHOD_1), MEAN_DATA_1),
+                INTERVAL_DATA))
+        .when(mockViewManager)
+        .getView(RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW.getName());
+    doReturn(
+            ViewData.create(
+                RPC_CLIENT_ERROR_COUNT_VIEW,
+                ImmutableMap.of(
+                    Arrays.asList(METHOD_1), MEAN_DATA_2, Arrays.asList(METHOD_2), MEAN_DATA_3),
+                CUMULATIVE_DATA))
+        .when(mockViewManager)
+        .getView(RPC_CLIENT_ERROR_COUNT_VIEW.getName());
+    doReturn(
+            ViewData.create(
+                RPC_CLIENT_REQUEST_BYTES_VIEW,
+                ImmutableMap.of(Arrays.asList(METHOD_1), DISTRIBUTION_DATA),
+                CUMULATIVE_DATA))
+        .when(mockViewManager)
+        .getView(RPC_CLIENT_REQUEST_BYTES_VIEW.getName());
+    OutputStream output = new ByteArrayOutputStream();
+    RpczZPageHandler handler = RpczZPageHandler.create(mockViewManager);
+    handler.emitHtml(Maps.newHashMap(), output);
+    assertThat(output.toString()).contains(METHOD_1.asString());
+    assertThat(output.toString()).contains(METHOD_2.asString());
+  }
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/StatszZPageHandlerTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/StatszZPageHandlerTest.java
new file mode 100644
index 0000000..81e64a6
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/StatszZPageHandlerTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2018, 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.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_ERROR_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_ROUNDTRIP_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_SERVER_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_LATENCY_VIEW;
+import static io.opencensus.contrib.zpages.StatszZPageHandler.QUERY_PATH;
+import static org.mockito.Mockito.doReturn;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link StatszZPageHandler}. */
+@RunWith(JUnit4.class)
+public class StatszZPageHandlerTest {
+
+  @Mock private final ViewManager mockViewManager = Mockito.mock(ViewManager.class);
+
+  private static final View MY_VIEW =
+      View.create(
+          View.Name.create("my_view"),
+          "My view",
+          RPC_CLIENT_REQUEST_BYTES,
+          Sum.create(),
+          Arrays.asList(TagKey.create("my_key")),
+          Cumulative.create());
+  private static final TagValue METHOD_1 = TagValue.create("method1");
+  private static final TagValue METHOD_2 = TagValue.create("method2");
+  private static final TagValue METHOD_3 = TagValue.create("method3");
+  private static final AggregationData.MeanData MEAN_DATA = AggregationData.MeanData.create(1, 3);
+  private static final AggregationData.DistributionData DISTRIBUTION_DATA_1 =
+      AggregationData.DistributionData.create(
+          4.2,
+          5,
+          0.2,
+          16.3,
+          234.56,
+          Arrays.asList(0L, 1L, 1L, 2L, 1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L));
+  private static final AggregationData.DistributionData DISTRIBUTION_DATA_2 =
+      AggregationData.DistributionData.create(
+          7.9,
+          11,
+          5.1,
+          12.2,
+          123.88,
+          Arrays.asList(0L, 0L, 3L, 5L, 3L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L));
+  private static final ViewData.AggregationWindowData.CumulativeData CUMULATIVE_DATA =
+      ViewData.AggregationWindowData.CumulativeData.create(
+          Timestamp.fromMillis(1000), Timestamp.fromMillis(5000));
+  private static final ViewData VIEW_DATA_1 =
+      ViewData.create(
+          RPC_CLIENT_REQUEST_BYTES_VIEW,
+          ImmutableMap.of(
+              Arrays.asList(METHOD_1), DISTRIBUTION_DATA_1,
+              Arrays.asList(METHOD_2), DISTRIBUTION_DATA_2),
+          CUMULATIVE_DATA);
+  private static final ViewData VIEW_DATA_2 =
+      ViewData.create(
+          RPC_CLIENT_ERROR_COUNT_VIEW,
+          ImmutableMap.of(Arrays.asList(METHOD_3), MEAN_DATA),
+          CUMULATIVE_DATA);
+
+  @Before
+  public void setUp() {
+    doReturn(
+            ImmutableSet.of(
+                RPC_CLIENT_REQUEST_BYTES_VIEW,
+                RPC_CLIENT_ERROR_COUNT_VIEW,
+                RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW,
+                RPC_SERVER_SERVER_LATENCY_VIEW,
+                MY_VIEW))
+        .when(mockViewManager)
+        .getAllExportedViews();
+    doReturn(VIEW_DATA_1)
+        .when(mockViewManager)
+        .getView(RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW.getName());
+    doReturn(VIEW_DATA_2).when(mockViewManager).getView(RPC_CLIENT_ERROR_COUNT_VIEW.getName());
+  }
+
+  @Test
+  public void getUrl() {
+    StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+    assertThat(handler.getUrlPath()).isEqualTo("/statsz");
+  }
+
+  @Test
+  public void emitMeasures() {
+    OutputStream output = new ByteArrayOutputStream();
+    StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+    handler.emitHtml(Maps.newHashMap(), output);
+    assertContainsMeasure(output, RPC_CLIENT_REQUEST_BYTES);
+    assertContainsMeasure(output, RPC_CLIENT_ERROR_COUNT);
+    assertContainsMeasure(output, RPC_CLIENT_ROUNDTRIP_LATENCY);
+    assertContainsMeasure(output, RPC_SERVER_SERVER_LATENCY);
+  }
+
+  @Test
+  public void emitDirectoriesAndViews() {
+    StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+
+    OutputStream output1 = new ByteArrayOutputStream();
+    handler.emitHtml(Maps.newHashMap(), output1);
+    assertThat(output1.toString()).contains("grpc.io");
+    assertThat(output1.toString()).contains("(4 views)");
+    assertThat(output1.toString()).contains("my_view");
+
+    OutputStream output2 = new ByteArrayOutputStream();
+    handler.emitHtml(ImmutableMap.of(QUERY_PATH, "/grpc.io"), output2);
+    assertThat(output2.toString()).contains("client");
+    assertThat(output2.toString()).contains("(3 views)");
+    assertThat(output2.toString()).contains("server");
+    assertThat(output2.toString()).contains("(1 view)");
+
+    OutputStream output3 = new ByteArrayOutputStream();
+    handler.emitHtml(ImmutableMap.of(QUERY_PATH, "/grpc.io/client"), output3);
+    assertThat(output3.toString()).contains("request_bytes");
+    assertThat(output3.toString()).contains("error_count");
+    assertThat(output3.toString()).contains("roundtrip_latency");
+    assertThat(output3.toString()).contains("(1 view)");
+  }
+
+  @Test
+  public void emitViewData() {
+    StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+
+    OutputStream output1 = new ByteArrayOutputStream();
+    handler.emitHtml(
+        ImmutableMap.of(QUERY_PATH, "/grpc.io/client/roundtrip_latency/cumulative"), output1);
+    assertContainsViewData(output1, VIEW_DATA_1);
+
+    OutputStream output2 = new ByteArrayOutputStream();
+    handler.emitHtml(
+        ImmutableMap.of(QUERY_PATH, "/grpc.io/client/error_count/cumulative"), output2);
+    assertContainsViewData(output2, VIEW_DATA_2);
+  }
+
+  @Test
+  public void nonExistingPath() {
+    StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+    OutputStream output = new ByteArrayOutputStream();
+    handler.emitHtml(ImmutableMap.of(QUERY_PATH, "/unknown/unknown_view"), output);
+    assertThat(output.toString())
+        .contains("Directory not found: /unknown/unknown_view. Return to root.");
+  }
+
+  @Test
+  public void viewWithNoStats() {
+    StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+    OutputStream output = new ByteArrayOutputStream();
+    handler.emitHtml(ImmutableMap.of(QUERY_PATH, "/my_view"), output);
+    assertThat(output.toString()).contains("No Stats found for View my_view.");
+  }
+
+  private static void assertContainsMeasure(OutputStream output, Measure measure) {
+    assertThat(output.toString()).contains(measure.getName());
+    assertThat(output.toString()).contains(measure.getDescription());
+    assertThat(output.toString()).contains(measure.getUnit());
+    String type =
+        measure.match(
+            Functions.returnConstant("Double"),
+            Functions.returnConstant("Long"),
+            Functions.throwAssertionError());
+    assertThat(output.toString()).contains(type);
+  }
+
+  private static void assertContainsViewData(OutputStream output, ViewData viewData) {
+    View view = viewData.getView();
+    assertThat(output.toString()).contains(view.getName().asString());
+    assertThat(output.toString()).contains(view.getDescription());
+    assertThat(output.toString()).contains(view.getMeasure().getName());
+    for (TagKey tagKey : view.getColumns()) {
+      assertThat(output.toString()).contains(tagKey.getName());
+    }
+    String aggregationType =
+        view.getAggregation()
+            .match(
+                Functions.returnConstant("Sum"),
+                Functions.returnConstant("Count"),
+                Functions.returnConstant("Distribution"),
+                Functions.returnConstant("Last Value"),
+                new Function<Aggregation, String>() {
+                  @Override
+                  public String apply(Aggregation arg) {
+                    if (arg instanceof Aggregation.Mean) {
+                      return "Mean";
+                    }
+                    throw new AssertionError();
+                  }
+                });
+    assertThat(output.toString()).contains(aggregationType);
+    for (Map.Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+        viewData.getAggregationMap().entrySet()) {
+      List<TagValue> tagValues = entry.getKey();
+      for (TagValue tagValue : tagValues) {
+        String tagValueStr = tagValue == null ? "" : tagValue.asString();
+        assertThat(output.toString()).contains(tagValueStr);
+      }
+      entry
+          .getValue()
+          .match(
+              Functions.</*@Nullable*/ Void>throwAssertionError(),
+              Functions.</*@Nullable*/ Void>throwAssertionError(),
+              Functions.</*@Nullable*/ Void>throwAssertionError(),
+              new Function<AggregationData.DistributionData, Void>() {
+                @Override
+                public Void apply(AggregationData.DistributionData arg) {
+                  assertThat(output.toString()).contains(String.valueOf(arg.getCount()));
+                  assertThat(output.toString()).contains(String.valueOf(arg.getMax()));
+                  assertThat(output.toString()).contains(String.valueOf(arg.getMin()));
+                  assertThat(output.toString()).contains(String.valueOf(arg.getMean()));
+                  assertThat(output.toString())
+                      .contains(String.valueOf(arg.getSumOfSquaredDeviations()));
+                  return null;
+                }
+              },
+              Functions.</*@Nullable*/ Void>throwAssertionError(),
+              Functions.</*@Nullable*/ Void>throwAssertionError(),
+              new Function<AggregationData, Void>() {
+                @Override
+                public Void apply(AggregationData arg) {
+                  if (arg instanceof MeanData) {
+                    MeanData meanData = (MeanData) arg;
+                    assertThat(output.toString()).contains(String.valueOf(meanData.getCount()));
+                    assertThat(output.toString()).contains(String.valueOf(meanData.getMean()));
+                    return null;
+                  }
+                  throw new AssertionError();
+                }
+              });
+    }
+  }
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/TracezZPageHandlerTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/TracezZPageHandlerTest.java
new file mode 100644
index 0000000..63ea8c4
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/TracezZPageHandlerTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SampledSpanStore;
+import io.opencensus.trace.export.SampledSpanStore.LatencyBucketBoundaries;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link TracezZPageHandler}. */
+@RunWith(JUnit4.class)
+public class TracezZPageHandlerTest {
+  private static final String ACTIVE_SPAN_NAME = "TestActiveSpan";
+  private static final String SAMPLED_SPAN_NAME = "TestSampledSpan";
+  private static final String ACTIVE_SAMPLED_SPAN_NAME = "TestActiveAndSampledSpan";
+  @Mock private RunningSpanStore runningSpanStore;
+  @Mock private SampledSpanStore sampledSpanStore;
+  RunningSpanStore.Summary runningSpanStoreSummary;
+  SampledSpanStore.Summary sampledSpanStoreSummary;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    Map<String, RunningSpanStore.PerSpanNameSummary> runningSummaryMap = new HashMap<>();
+    runningSummaryMap.put(ACTIVE_SPAN_NAME, RunningSpanStore.PerSpanNameSummary.create(3));
+    runningSummaryMap.put(ACTIVE_SAMPLED_SPAN_NAME, RunningSpanStore.PerSpanNameSummary.create(5));
+    runningSpanStoreSummary = RunningSpanStore.Summary.create(runningSummaryMap);
+    Map<LatencyBucketBoundaries, Integer> numbersOfLatencySampledSpans = new HashMap<>();
+    numbersOfLatencySampledSpans.put(LatencyBucketBoundaries.MILLIx1_MILLIx10, 3);
+    numbersOfLatencySampledSpans.put(LatencyBucketBoundaries.MICROSx10_MICROSx100, 7);
+    Map<CanonicalCode, Integer> numbersOfErrorSampledSpans = new HashMap<>();
+    numbersOfErrorSampledSpans.put(CanonicalCode.CANCELLED, 2);
+    numbersOfErrorSampledSpans.put(CanonicalCode.DEADLINE_EXCEEDED, 5);
+    Map<String, SampledSpanStore.PerSpanNameSummary> sampledSummaryMap = new HashMap<>();
+    sampledSummaryMap.put(
+        SAMPLED_SPAN_NAME,
+        SampledSpanStore.PerSpanNameSummary.create(
+            numbersOfLatencySampledSpans, numbersOfErrorSampledSpans));
+    sampledSummaryMap.put(
+        ACTIVE_SAMPLED_SPAN_NAME,
+        SampledSpanStore.PerSpanNameSummary.create(
+            numbersOfLatencySampledSpans, numbersOfErrorSampledSpans));
+    sampledSpanStoreSummary = SampledSpanStore.Summary.create(sampledSummaryMap);
+  }
+
+  @Test
+  public void emitSummaryTableForEachSpan() {
+    OutputStream output = new ByteArrayOutputStream();
+    TracezZPageHandler tracezZPageHandler =
+        TracezZPageHandler.create(runningSpanStore, sampledSpanStore);
+    when(runningSpanStore.getSummary()).thenReturn(runningSpanStoreSummary);
+    when(sampledSpanStore.getSummary()).thenReturn(sampledSpanStoreSummary);
+    tracezZPageHandler.emitHtml(Collections.emptyMap(), output);
+    assertThat(output.toString()).contains(ACTIVE_SPAN_NAME);
+    assertThat(output.toString()).contains(SAMPLED_SPAN_NAME);
+    assertThat(output.toString()).contains(ACTIVE_SAMPLED_SPAN_NAME);
+  }
+
+  @Test
+  public void linksForActiveRequests_InSummaryTable() {
+    OutputStream output = new ByteArrayOutputStream();
+    TracezZPageHandler tracezZPageHandler =
+        TracezZPageHandler.create(runningSpanStore, sampledSpanStore);
+    when(runningSpanStore.getSummary()).thenReturn(runningSpanStoreSummary);
+    when(sampledSpanStore.getSummary()).thenReturn(sampledSpanStoreSummary);
+    tracezZPageHandler.emitHtml(Collections.emptyMap(), output);
+    // 3 active requests
+    assertThat(output.toString()).contains("href='?zspanname=TestActiveSpan&ztype=0&zsubtype=0'>3");
+    // No active links
+    assertThat(output.toString())
+        .doesNotContain("href='?zspanname=TestSampledSpan&ztype=0&zsubtype=0'");
+    // 5 active requests
+    assertThat(output.toString())
+        .contains("href='?zspanname=TestActiveAndSampledSpan&ztype=0&zsubtype=0'>5");
+  }
+
+  @Test
+  public void linksForSampledRequests_InSummaryTable() {
+    OutputStream output = new ByteArrayOutputStream();
+    TracezZPageHandler tracezZPageHandler =
+        TracezZPageHandler.create(runningSpanStore, sampledSpanStore);
+    when(runningSpanStore.getSummary()).thenReturn(runningSpanStoreSummary);
+    when(sampledSpanStore.getSummary()).thenReturn(sampledSpanStoreSummary);
+    tracezZPageHandler.emitHtml(Collections.emptyMap(), output);
+    // No sampled links (ztype=1);
+    assertThat(output.toString()).doesNotContain("href=\"?zspanname=TestActiveSpan&ztype=1");
+    // Links for 7 samples [10us, 100us) and 3 samples [1ms, 10ms);
+    assertThat(output.toString())
+        .contains("href='?zspanname=TestSampledSpan&ztype=1&zsubtype=1'>7");
+    assertThat(output.toString())
+        .contains("href='?zspanname=TestSampledSpan&ztype=1&zsubtype=3'>3");
+    // Links for 7 samples [10us, 100us) and 3 samples [1ms, 10ms);
+    assertThat(output.toString())
+        .contains("href='?zspanname=TestActiveAndSampledSpan&ztype=1&zsubtype=1'>7");
+    assertThat(output.toString())
+        .contains("href='?zspanname=TestActiveAndSampledSpan&ztype=1&zsubtype=3'>3");
+  }
+
+  @Test
+  public void linksForFailedRequests_InSummaryTable() {
+    OutputStream output = new ByteArrayOutputStream();
+    TracezZPageHandler tracezZPageHandler =
+        TracezZPageHandler.create(runningSpanStore, sampledSpanStore);
+    when(runningSpanStore.getSummary()).thenReturn(runningSpanStoreSummary);
+    when(sampledSpanStore.getSummary()).thenReturn(sampledSpanStoreSummary);
+    tracezZPageHandler.emitHtml(Collections.emptyMap(), output);
+    // No sampled links (ztype=1);
+    assertThat(output.toString()).doesNotContain("href=\"?zspanname=TestActiveSpan&ztype=2");
+    // Links for 7 errors 2 CANCELLED + 5 DEADLINE_EXCEEDED;
+    assertThat(output.toString())
+        .contains("href='?zspanname=TestSampledSpan&ztype=2&zsubtype=0'>7");
+    // Links for 7 errors 2 CANCELLED + 5 DEADLINE_EXCEEDED;
+    assertThat(output.toString())
+        .contains("href='?zspanname=TestActiveAndSampledSpan&ztype=2&zsubtype=0'>7");
+  }
+
+  // TODO(bdrutu): Add tests for latency.
+  // TODO(bdrutu): Add tests for samples/running/errors.
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHandlersTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHandlersTest.java
new file mode 100644
index 0000000..a7bbf11
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHandlersTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ZPageHandlers}. */
+@RunWith(JUnit4.class)
+public class ZPageHandlersTest {
+
+  @Test
+  public void implementationOfTracez() {
+    assertThat(ZPageHandlers.getTracezZPageHandler()).isInstanceOf(TracezZPageHandler.class);
+  }
+
+  @Test
+  public void implementationOfTraceConfigz() {
+    assertThat(ZPageHandlers.getTraceConfigzZPageHandler())
+        .isInstanceOf(TraceConfigzZPageHandler.class);
+  }
+
+  @Test
+  public void implementationOfRpcz() {
+    assertThat(ZPageHandlers.getRpczZpageHandler()).isInstanceOf(RpczZPageHandler.class);
+  }
+
+  @Test
+  public void implementationOfStatsz() {
+    assertThat(ZPageHandlers.getStatszZPageHandler()).isInstanceOf(StatszZPageHandler.class);
+  }
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHttpHandlerTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHttpHandlerTest.java
new file mode 100644
index 0000000..7ac5ba6
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHttpHandlerTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ZPageHttpHandler}. */
+@RunWith(JUnit4.class)
+public class ZPageHttpHandlerTest {
+  @Test
+  public void parseUndefinedQuery() throws URISyntaxException {
+    URI uri = new URI("http://localhost:8000/tracez");
+    assertThat(ZPageHttpHandler.uriQueryToMap(uri)).isEmpty();
+  }
+
+  @Test
+  public void parseQuery() throws URISyntaxException {
+    URI uri = new URI("http://localhost:8000/tracez?ztype=1&zsubtype&zname=Test");
+    assertThat(ZPageHttpHandler.uriQueryToMap(uri))
+        .containsExactly("ztype", "1", "zsubtype", "", "zname", "Test");
+  }
+}
diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel
new file mode 100644
index 0000000..a93cefd
--- /dev/null
+++ b/examples/BUILD.bazel
@@ -0,0 +1,155 @@
+load("//:opencensus_workspace.bzl", "opencensus_java_libraries")
+load("@grpc_java//:java_grpc_library.bzl", "java_grpc_library")
+
+opencensus_java_libraries()
+
+proto_library(
+    name = "helloworld_proto",
+    srcs = ["src/main/proto/helloworld.proto"],
+)
+
+java_proto_library(
+    name = "helloworld_java_proto",
+    deps = [":helloworld_proto"],
+)
+
+java_grpc_library(
+    name = "helloworld_java_grpc",
+    srcs = [":helloworld_proto"],
+    deps = [":helloworld_java_proto"],
+)
+
+java_library(
+    name = "opencensus_examples",
+    srcs = glob(
+        ["src/main/java/**/*.java"],
+    ),
+    deps = [
+        ":helloworld_java_grpc",
+        ":helloworld_java_proto",
+        "@com_google_guava_guava//jar",
+        "@com_google_code_findbugs_jsr305//jar",
+        "@io_opencensus_opencensus_api//jar",
+        "@io_opencensus_opencensus_contrib_grpc_metrics//jar",
+        "@io_opencensus_opencensus_contrib_zpages//jar",
+        "@io_opencensus_opencensus_exporter_stats_prometheus//jar",
+        "@io_opencensus_opencensus_exporter_stats_stackdriver//jar",
+        "@io_opencensus_opencensus_exporter_trace_logging//jar",
+        "@io_opencensus_opencensus_exporter_trace_stackdriver//jar",
+        "@io_grpc_grpc_core//jar",
+        "@io_grpc_grpc_netty//jar",
+        "@io_grpc_grpc_protobuf//jar",
+        "@io_grpc_grpc_stub//jar",
+        "@io_prometheus_simpleclient//jar",
+        "@io_prometheus_simpleclient_httpserver//jar",
+    ],
+    runtime_deps = [
+        "@com_google_api_api_common//jar",
+        "@com_google_api_gax//jar",
+        "@com_google_api_gax_grpc//jar",
+        "@com_google_api_grpc_proto_google_cloud_trace_v1//jar",
+        "@com_google_api_grpc_proto_google_cloud_trace_v2//jar",
+        "@com_google_api_grpc_proto_google_iam_v1//jar",
+        "@com_google_api_grpc_proto_google_cloud_monitoring_v3//jar",
+        "@com_google_api_grpc_proto_google_common_protos//jar",
+        "@com_google_auth_google_auth_library_credentials//jar",
+        "@com_google_auth_google_auth_library_oauth2_http//jar",
+        "@com_google_cloud_google_cloud_core//jar",
+        "@com_google_cloud_google_cloud_core_grpc//jar",
+        "@com_google_cloud_google_cloud_monitoring//jar",
+        "@com_google_cloud_google_cloud_trace//jar",
+        "@com_google_http_client_google_http_client//jar",
+        "@com_google_http_client_google_http_client_jackson2//jar",
+        "@com_google_instrumentation_instrumentation_api//jar",
+        "@com_google_protobuf_protobuf_java//jar",
+        "@com_google_protobuf_protobuf_java_util//jar",
+        "@commons_codec_commons_codec//jar",
+        "@commons_logging_commons_logging//jar",
+
+        "@com_lmax_disruptor//jar",
+        "@io_grpc_grpc_context//jar",
+        "@io_grpc_grpc_auth//jar",
+        "@io_grpc_grpc_protobuf_lite//jar",
+        "@io_netty_netty_buffer//jar",
+        "@io_netty_netty_common//jar",
+        "@io_netty_netty_codec//jar",
+        "@io_netty_netty_codec_socks//jar",
+        "@io_netty_netty_codec_http//jar",
+        "@io_netty_netty_codec_http2//jar",
+        "@io_netty_netty_handler//jar",
+        "@io_netty_netty_handler_proxy//jar",
+        "@io_netty_netty_resolver//jar",
+        "@io_netty_netty_tcnative_boringssl_static//jar",
+        "@io_netty_netty_transport//jar",
+        "@io_opencensus_opencensus_impl//jar",
+        "@io_opencensus_opencensus_impl_core//jar",
+        "@joda_time_joda_time//jar",
+        "@org_apache_httpcomponents_httpclient//jar",
+        "@org_apache_httpcomponents_httpcore//jar",
+        "@org_threeten_threetenbp//jar",
+    ],
+)
+
+java_binary(
+    name = "TagContextExample",
+    main_class = "io.opencensus.examples.tags.TagContextExample",
+    runtime_deps = [
+        ":opencensus_examples",
+    ],
+)
+
+java_binary(
+    name = "MultiSpansTracing",
+    main_class = "io.opencensus.examples.trace.MultiSpansTracing",
+    runtime_deps = [
+        ":opencensus_examples",
+    ],
+)
+
+java_binary(
+    name = "MultiSpansScopedTracing",
+    main_class = "io.opencensus.examples.trace.MultiSpansScopedTracing",
+    runtime_deps = [
+        ":opencensus_examples",
+    ],
+)
+
+java_binary(
+    name = "MultiSpansContextTracing",
+    main_class = "io.opencensus.examples.trace.MultiSpansContextTracing",
+    runtime_deps = [
+        ":opencensus_examples",
+    ],
+)
+
+java_binary(
+    name = "ZPagesTester",
+    main_class = "io.opencensus.examples.zpages.ZPagesTester",
+    runtime_deps = [
+        ":opencensus_examples",
+    ],
+)
+
+java_binary(
+    name = "QuickStart",
+    main_class = "io.opencensus.examples.helloworld.QuickStart",
+    runtime_deps = [
+        ":opencensus_examples",
+    ],
+)
+
+java_binary(
+    name = "HelloWorldClient",
+    main_class = "io.opencensus.examples.grpc.helloworld.HelloWorldClient",
+    runtime_deps = [
+        ":opencensus_examples",
+    ],
+)
+
+java_binary(
+    name = "HelloWorldServer",
+    main_class = "io.opencensus.examples.grpc.helloworld.HelloWorldServer",
+    runtime_deps = [
+        ":opencensus_examples",
+    ],
+)
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..921691b
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,113 @@
+# OpenCensus Examples
+
+## To build the examples use
+
+### Gradle
+```
+$ ./gradlew installDist
+```
+
+### Maven
+```
+$ mvn package appassembler:assemble
+```
+
+### Bazel
+```
+$ bazel build :all
+```
+
+## To run "TagContextExample" use
+
+### Gradle
+```
+$ ./build/install/opencensus-examples/bin/TagContextExample
+```
+
+### Maven
+```
+$ ./target/appassembler/bin/TagContextExample
+```
+
+### Bazel
+```
+$ ./bazel-bin/TagContextExample
+```
+
+## To run "ZPagesTester"
+
+### Gradle
+```
+$ ./build/install/opencensus-examples/bin/ZPagesTester
+```
+
+### Maven
+```
+$ ./target/appassembler/bin/ZPagesTester
+```
+
+### Bazel
+```
+$ ./bazel-bin/ZPagesTester
+```
+
+Available pages:
+* For tracing page go to [localhost:8080/tracez][ZPagesTraceZLink]. 
+* For tracing config page go to [localhost:8080/traceconfigz][ZPagesTraceConfigZLink].
+* For RPC stats page go to [localhost:8080/rpcz][ZPagesRpcZLink].
+* For stats and measures on all registered views go to [localhost:8080/statsz][ZPagesStatsZLink].
+
+[ZPagesTraceZLink]: http://localhost:8080/tracez
+[ZPagesTraceConfigZLink]: http://localhost:8080/traceconfigz
+[ZPagesRpcZLink]: http://localhost:8080/rpcz
+[ZPagesStatsZLink]: http://localhost:8080/statsz
+
+## To run "QuickStart" example use
+
+### Gradle
+```
+$ ./build/install/opencensus-examples/bin/QuickStart
+```
+
+### Maven
+```
+$ ./target/appassembler/bin/QuickStart
+```
+
+### Bazel
+```
+$ ./bazel-bin/QuickStart
+```
+
+## To run "gRPC Hello World" example use
+
+Please note all the arguments are optional. If you do not specify these arguments, default values
+will be used:
+
+* host and serverPort will be "localhost:50051"
+* user will be "world"
+* cloudProjectId will be null (which means no stats/spans will be exported to Stackdriver)
+* server zPagePort will be 3000
+* client zPagePort will be 3001
+* Prometheus port will be 9090
+
+
+However, if you want to specify any of these arguements, please make sure they are in order.
+
+### Gradle
+```
+$ ./build/install/opencensus-examples/bin/HelloWorldServer serverPort cloudProjectId zPagePort prometheusPort
+$ ./build/install/opencensus-examples/bin/HelloWorldClient user host serverPort cloudProjectId zPagePort
+```
+
+### Maven
+```
+$ ./target/appassembler/bin/HelloWorldServer serverPort cloudProjectId zPagePort prometheusPort
+$ ./target/appassembler/bin/HelloWorldClient user host serverPort cloudProjectId zPagePort
+```
+
+### Bazel
+```
+$ ./bazel-bin/HelloWorldServer serverPort cloudProjectId zPagePort prometheusPort
+$ ./bazel-bin/HelloWorldClient user host serverPort cloudProjectId zPagePort
+```
diff --git a/examples/WORKSPACE b/examples/WORKSPACE
new file mode 100644
index 0000000..a065f96
--- /dev/null
+++ b/examples/WORKSPACE
@@ -0,0 +1,53 @@
+workspace(name = "opencensus_examples")
+
+git_repository(
+    name = "grpc_java",
+    remote = "https://github.com/grpc/grpc-java.git",
+    tag = "v1.10.1",
+)
+
+load("//:opencensus_workspace.bzl", "opencensus_maven_jars")
+load("@grpc_java//:repositories.bzl", "grpc_java_repositories")
+
+opencensus_maven_jars()
+grpc_java_repositories(
+    # Omit to avoid conflicts.
+
+    omit_com_google_auth_google_auth_library_credentials=True,
+    omit_com_google_api_grpc_google_common_protos=True,
+    omit_com_google_code_findbugs_jsr305=True,
+    omit_com_google_code_gson=True,
+    omit_com_google_errorprone_error_prone_annotations=True,
+    omit_com_google_guava=True,
+    omit_com_google_protobuf=True,
+    omit_com_google_protobuf_nano_protobuf_javanano=True,
+    omit_com_google_truth_truth=True,
+    omit_com_squareup_okhttp=True,
+    omit_com_squareup_okio=True,
+
+    # These netty dependencies have already been included in opencensus_workspace.bzl
+    omit_io_netty_buffer=True,
+    omit_io_netty_common=True,
+    omit_io_netty_handler_proxy=True,
+    omit_io_netty_codec_http2=True,
+    omit_io_netty_transport=True,
+    omit_io_netty_codec=True,
+    omit_io_netty_codec_socks=True,
+    omit_io_netty_codec_http=True,
+    omit_io_netty_handler=True,
+    omit_io_netty_resolver=True,
+
+    omit_io_opencensus_api=True,
+    omit_io_opencensus_grpc_metrics=True,
+    omit_junit_junit=True
+)
+
+# proto_library, cc_proto_library, and java_proto_library rules implicitly
+# depend on @com_google_protobuf for protoc and proto runtimes.
+# This statement defines the @com_google_protobuf repo.
+http_archive(
+    name = "com_google_protobuf",
+    sha256 = "1f8b9b202e9a4e467ff0b0f25facb1642727cdf5e69092038f15b37c75b99e45",
+    strip_prefix = "protobuf-3.5.1",
+    urls = ["https://github.com/google/protobuf/archive/v3.5.1.zip"],
+)
diff --git a/examples/build.gradle b/examples/build.gradle
new file mode 100644
index 0000000..22889e1
--- /dev/null
+++ b/examples/build.gradle
@@ -0,0 +1,154 @@
+description = 'OpenCensus Examples'
+
+buildscript {
+    repositories {
+        mavenCentral()
+        mavenLocal()
+        maven {
+            url "https://plugins.gradle.org/m2/"
+        }
+    }
+    dependencies {
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.3'
+    }
+}
+
+apply plugin: 'idea'
+apply plugin: 'java'
+apply plugin: 'com.google.protobuf'
+
+repositories {
+    mavenCentral()
+    mavenLocal()
+}
+
+group = "io.opencensus"
+version = "0.17.0-SNAPSHOT" // CURRENT_OPENCENSUS_VERSION
+
+def opencensusVersion = "0.16.1" // LATEST_OPENCENSUS_RELEASE_VERSION
+def grpcVersion = "1.13.1" // CURRENT_GRPC_VERSION
+def prometheusVersion = "0.3.0"
+
+tasks.withType(JavaCompile) {
+    sourceCompatibility = '1.8'
+    targetCompatibility = '1.8'
+}
+
+dependencies {
+    compile "com.google.api.grpc:proto-google-common-protos:1.11.0",
+            "io.opencensus:opencensus-api:${opencensusVersion}",
+            "io.opencensus:opencensus-contrib-zpages:${opencensusVersion}",
+            "io.opencensus:opencensus-contrib-grpc-metrics:${opencensusVersion}",
+            "io.opencensus:opencensus-exporter-stats-prometheus:${opencensusVersion}",
+            "io.opencensus:opencensus-exporter-stats-stackdriver:${opencensusVersion}",
+            "io.opencensus:opencensus-exporter-trace-stackdriver:${opencensusVersion}",
+            "io.opencensus:opencensus-exporter-trace-logging:${opencensusVersion}",
+            "io.grpc:grpc-protobuf:${grpcVersion}",
+            "io.grpc:grpc-stub:${grpcVersion}",
+            "io.grpc:grpc-netty:${grpcVersion}",
+            "io.prometheus:simpleclient_httpserver:${prometheusVersion}"
+
+    runtime "io.opencensus:opencensus-impl:${opencensusVersion}",
+            "io.netty:netty-tcnative-boringssl-static:2.0.8.Final"
+}
+
+protobuf {
+    protoc {
+        artifact = 'com.google.protobuf:protoc:3.5.1-1'
+    }
+    plugins {
+        grpc {
+            artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
+        }
+    }
+    generateProtoTasks {
+        all()*.plugins {
+            grpc {}
+        }
+        ofSourceSet('main')
+    }
+}
+
+// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code.
+sourceSets {
+    main {
+        java {
+            srcDir 'src'
+            srcDirs 'build/generated/source/proto/main/grpc'
+            srcDirs 'build/generated/source/proto/main/java'
+        }
+    }
+}
+
+// Provide convenience executables for trying out the examples.
+apply plugin: 'application'
+
+startScripts.enabled = false
+
+task tagContextExample(type: CreateStartScripts) {
+    mainClassName = 'io.opencensus.examples.tags.TagContextExample'
+    applicationName = 'TagContextExample'
+    outputDir = new File(project.buildDir, 'tmp')
+    classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task multiSpansTracing(type: CreateStartScripts) {
+    mainClassName = 'io.opencensus.examples.trace.MultiSpansTracing'
+    applicationName = 'MultiSpansTracing'
+    outputDir = new File(project.buildDir, 'tmp')
+    classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task multiSpansScopedTracing(type: CreateStartScripts) {
+    mainClassName = 'io.opencensus.examples.trace.MultiSpansScopedTracing'
+    applicationName = 'MultiSpansScopedTracing'
+    outputDir = new File(project.buildDir, 'tmp')
+    classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task multiSpansContextTracing(type: CreateStartScripts) {
+    mainClassName = 'io.opencensus.examples.trace.MultiSpansContextTracing'
+    applicationName = 'MultiSpansContextTracing'
+    outputDir = new File(project.buildDir, 'tmp')
+    classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task zPagesTester(type: CreateStartScripts) {
+    mainClassName = 'io.opencensus.examples.zpages.ZPagesTester'
+    applicationName = 'ZPagesTester'
+    outputDir = new File(project.buildDir, 'tmp')
+    classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task quickStart(type: CreateStartScripts) {
+    mainClassName = 'io.opencensus.examples.helloworld.QuickStart'
+    applicationName = 'QuickStart'
+    outputDir = new File(project.buildDir, 'tmp')
+    classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task helloWorldServer(type: CreateStartScripts) {
+    mainClassName = 'io.opencensus.examples.grpc.helloworld.HelloWorldServer'
+    applicationName = 'HelloWorldServer'
+    outputDir = new File(project.buildDir, 'tmp')
+    classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task helloWorldClient(type: CreateStartScripts) {
+    mainClassName = 'io.opencensus.examples.grpc.helloworld.HelloWorldClient'
+    applicationName = 'HelloWorldClient'
+    outputDir = new File(project.buildDir, 'tmp')
+    classpath = jar.outputs.files + project.configurations.runtime
+}
+
+applicationDistribution.into('bin') {
+    from(multiSpansTracing)
+    from(multiSpansScopedTracing)
+    from(multiSpansContextTracing)
+    from(tagContextExample)
+    from(zPagesTester)
+    from(quickStart)
+    from(helloWorldServer)
+    from(helloWorldClient)
+    fileMode = 0755
+}
diff --git a/examples/gradle/wrapper/gradle-wrapper.jar b/examples/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..758de96
--- /dev/null
+++ b/examples/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a95009c
--- /dev/null
+++ b/examples/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/examples/gradlew b/examples/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/examples/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/examples/gradlew.bat b/examples/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/examples/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windows variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/examples/opencensus_workspace.bzl b/examples/opencensus_workspace.bzl
new file mode 100644
index 0000000..ce382cd
--- /dev/null
+++ b/examples/opencensus_workspace.bzl
@@ -0,0 +1,1680 @@
+# The following dependencies were calculated from:
+#
+# generate_workspace --artifact=com.google.guava:guava-jdk5:23.0 --artifact=com.google.guava:guava:23.0 --artifact=io.grpc:grpc-all:1.9.0 --artifact=io.opencensus:opencensus-api:0.16.1 --artifact=io.opencensus:opencensus-contrib-grpc-metrics:0.16.1 --artifact=io.opencensus:opencensus-contrib-zpages:0.16.1 --artifact=io.opencensus:opencensus-exporter-stats-prometheus:0.16.1 --artifact=io.opencensus:opencensus-exporter-stats-stackdriver:0.16.1 --artifact=io.opencensus:opencensus-exporter-trace-logging:0.16.1 --artifact=io.opencensus:opencensus-exporter-trace-stackdriver:0.16.1 --artifact=io.opencensus:opencensus-impl:0.16.1 --artifact=io.prometheus:simpleclient_httpserver:0.3.0 --repositories=http://repo.maven.apache.org/maven2
+
+
+def opencensus_maven_jars():
+  # io.opencensus:opencensus-api:jar:0.10.0 wanted version 3.0.1
+  # io.grpc:grpc-core:jar:1.9.0 wanted version 3.0.0
+  # com.google.guava:guava:bundle:23.0
+  # com.google.instrumentation:instrumentation-api:jar:0.4.3 wanted version 3.0.0
+  # io.opencensus:opencensus-contrib-grpc-metrics:jar:0.10.0 wanted version 3.0.1
+  native.maven_jar(
+      name = "com_google_code_findbugs_jsr305",
+      artifact = "com.google.code.findbugs:jsr305:2.0.2",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "516c03b21d50a644d538de0f0369c620989cd8f0",
+  )
+
+
+  # io.grpc:grpc-protobuf:jar:1.9.0
+  native.maven_jar(
+      name = "io_grpc_grpc_protobuf_lite",
+      artifact = "io.grpc:grpc-protobuf-lite:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "9dc9c6531ae0b304581adff0e9b7cff21a4073ac",
+  )
+
+
+  native.maven_jar(
+      name = "io_opencensus_opencensus_exporter_stats_prometheus",
+      artifact = "io.opencensus:opencensus-exporter-stats-prometheus:0.16.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "c1e9fc26da3060dde5a5948fd065c1b28cd10f39",
+  )
+
+
+  # com.google.api:gax-grpc:jar:1.30.0 got requested version
+  # com.google.api:gax:jar:1.30.0
+  native.maven_jar(
+      name = "com_google_auth_google_auth_library_oauth2_http",
+      artifact = "com.google.auth:google-auth-library-oauth2-http:0.10.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "c079a62086121973a23d90f54e2b8c13050fa39d",
+  )
+
+
+  # io.netty:netty-handler-proxy:jar:4.1.17.Final got requested version
+  # io.netty:netty-codec:jar:4.1.17.Final
+  # io.netty:netty-handler:jar:4.1.17.Final got requested version
+  native.maven_jar(
+      name = "io_netty_netty_transport",
+      artifact = "io.netty:netty-transport:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "9585776b0a8153182412b5d5366061ff486914c1",
+  )
+
+
+  # io.grpc:grpc-netty:jar:1.9.0
+  native.maven_jar(
+      name = "io_netty_netty_handler_proxy",
+      artifact = "io.netty:netty-handler-proxy:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "9330ee60c4e48ca60aac89b7bc5ec2567e84f28e",
+  )
+
+
+  # io.grpc:grpc-all:jar:1.9.0
+  native.maven_jar(
+      name = "io_grpc_grpc_protobuf_nano",
+      artifact = "io.grpc:grpc-protobuf-nano:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "561b03d3fd5178117a51f9f7ef9d9e5442ed2348",
+  )
+
+
+  # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1
+  native.maven_jar(
+      name = "com_google_cloud_google_cloud_trace",
+      artifact = "com.google.cloud:google-cloud-trace:0.58.0-beta",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "ea715c51340a32106ffdf32375a5dad9dbdf160e",
+  )
+
+
+  # org.apache.httpcomponents:httpclient:jar:4.5.3
+  native.maven_jar(
+      name = "commons_codec_commons_codec",
+      artifact = "commons-codec:commons-codec:1.9",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "9ce04e34240f674bc72680f8b843b1457383161a",
+  )
+
+
+  # io.opencensus:opencensus-impl:jar:0.16.1
+  native.maven_jar(
+      name = "io_opencensus_opencensus_impl_core",
+      artifact = "io.opencensus:opencensus-impl-core:0.16.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "a87fc041f66b8c923e2a1de6b7c1582b7990fde8",
+  )
+
+
+  # io.prometheus:simpleclient_httpserver:bundle:0.4.0
+  native.maven_jar(
+      name = "io_prometheus_simpleclient_common",
+      artifact = "io.prometheus:simpleclient_common:0.3.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "c9656d515d3a7647407f2c221d56be13177b82a0",
+  )
+
+
+  # com.google.api:gax-grpc:jar:1.30.0 got requested version
+  # com.google.api:gax:jar:1.30.0
+  native.maven_jar(
+      name = "org_threeten_threetenbp",
+      artifact = "org.threeten:threetenbp:1.3.3",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "3ea31c96676ff12ab56be0b1af6fff61d1a4f1f2",
+  )
+
+
+  # io.grpc:grpc-core:jar:1.9.0 wanted version 2.1.2
+  # io.opencensus:opencensus-contrib-grpc-metrics:jar:0.10.0 wanted version 2.1.2
+  # com.google.guava:guava:bundle:23.0
+  # io.opencensus:opencensus-api:jar:0.10.0 wanted version 2.1.2
+  native.maven_jar(
+      name = "com_google_errorprone_error_prone_annotations",
+      artifact = "com.google.errorprone:error_prone_annotations:2.0.18",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "5f65affce1684999e2f4024983835efc3504012e",
+  )
+
+
+  # io.netty:netty-transport:jar:4.1.17.Final
+  native.maven_jar(
+      name = "io_netty_netty_resolver",
+      artifact = "io.netty:netty-resolver:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "8f386c80821e200f542da282ae1d3cde5cad8368",
+  )
+
+
+  # com.squareup.okhttp:okhttp:jar:2.5.0
+  # io.grpc:grpc-okhttp:jar:1.9.0 wanted version 1.13.0
+  native.maven_jar(
+      name = "com_squareup_okio_okio",
+      artifact = "com.squareup.okio:okio:1.6.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "98476622f10715998eacf9240d6b479f12c66143",
+  )
+
+
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 3.6.0
+  # io.grpc:grpc-protobuf:jar:1.9.0
+  # com.google.cloud:google-cloud-core:jar:1.40.0 wanted version 3.6.0
+  native.maven_jar(
+      name = "com_google_protobuf_protobuf_java_util",
+      artifact = "com.google.protobuf:protobuf-java-util:3.5.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "6e40a6a3f52455bd633aa2a0dba1a416e62b4575",
+  )
+
+
+  # io.grpc:grpc-auth:jar:1.9.0
+  # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1 wanted version 0.10.0
+  # com.google.api:gax-grpc:jar:1.30.0 wanted version 0.10.0
+  # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1 wanted version 0.10.0
+  # com.google.auth:google-auth-library-oauth2-http:jar:0.9.0 got requested version
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 0.10.0
+  native.maven_jar(
+      name = "com_google_auth_google_auth_library_credentials",
+      artifact = "com.google.auth:google-auth-library-credentials:0.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "8e2b181feff6005c9cbc6f5c1c1e2d3ec9138d46",
+  )
+
+
+  # com.google.api.grpc:proto-google-cloud-trace-v2:jar:0.23.0 got requested version
+  # com.google.api:gax:jar:1.30.0 got requested version
+  # com.google.api.grpc:proto-google-cloud-trace-v1:jar:0.23.0 got requested version
+  # com.google.api.grpc:proto-google-iam-v1:jar:0.12.0 wanted version 1.5.0
+  # com.google.api.grpc:proto-google-cloud-monitoring-v3:jar:1.22.0 got requested version
+  # com.google.cloud:google-cloud-core:jar:1.40.0
+  # com.google.api:gax-grpc:jar:1.30.0 got requested version
+  native.maven_jar(
+      name = "com_google_api_api_common",
+      artifact = "com.google.api:api-common:1.7.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "ea59fb8b2450999345035dec8a6f472543391766",
+  )
+
+
+  # io.opencensus:opencensus-contrib-zpages:jar:0.16.1 got requested version
+  native.maven_jar(
+      name = "io_opencensus_opencensus_contrib_grpc_metrics",
+      artifact = "io.opencensus:opencensus-contrib-grpc-metrics:0.16.1",
+      sha1 = "f56b444e2766eaf597ee11c7501f0d6b9992395c",
+  )
+
+
+  # org.mockito:mockito-core:jar:1.9.5
+  native.maven_jar(
+      name = "org_objenesis_objenesis",
+      artifact = "org.objenesis:objenesis:1.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "9b473564e792c2bdf1449da1f0b1b5bff9805704",
+  )
+
+
+  # io.netty:netty-buffer:jar:4.1.17.Final
+  # io.netty:netty-resolver:jar:4.1.17.Final got requested version
+  native.maven_jar(
+      name = "io_netty_netty_common",
+      artifact = "io.netty:netty-common:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "581c8ee239e4dc0976c2405d155f475538325098",
+  )
+
+
+  # com.google.cloud:google-cloud-trace:jar:0.58.0-beta
+  native.maven_jar(
+      name = "com_google_api_grpc_proto_google_cloud_trace_v2",
+      artifact = "com.google.api.grpc:proto-google-cloud-trace-v2:0.23.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "4aa1bc7212d34791a02962092deafc43a7f4245e",
+  )
+
+
+  # com.google.cloud:google-cloud-trace:jar:0.58.0-beta got requested version
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0
+  # com.google.cloud:google-cloud-monitoring:jar:1.40.0 got requested version
+  native.maven_jar(
+      name = "io_grpc_grpc_netty_shaded",
+      artifact = "io.grpc:grpc-netty-shaded:1.13.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "ccdc4f2c2791d93164c574fbfb90d614aa0849ae",
+  )
+
+
+  # com.google.cloud:google-cloud-trace:jar:0.58.0-beta
+  native.maven_jar(
+      name = "com_google_api_grpc_proto_google_cloud_trace_v1",
+      artifact = "com.google.api.grpc:proto-google-cloud-trace-v1:0.23.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "848bb2c3b9d683dccc2a26d077015cdc71b7e343",
+  )
+
+
+  # io.grpc:grpc-all:jar:1.9.0
+  native.maven_jar(
+      name = "io_grpc_grpc_okhttp",
+      artifact = "io.grpc:grpc-okhttp:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "4e7fbb9d3cd65848f42494de165b1c5839f69a8a",
+  )
+
+
+  # junit:junit:jar:4.12
+  native.maven_jar(
+      name = "org_hamcrest_hamcrest_core",
+      artifact = "org.hamcrest:hamcrest-core:1.3",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
+  )
+
+
+  # io.netty:netty-codec-http2:jar:4.1.17.Final
+  native.maven_jar(
+      name = "io_netty_netty_handler",
+      artifact = "io.netty:netty-handler:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "18c40ffb61a1d1979eca024087070762fdc4664a",
+  )
+
+
+  # com.google.cloud:google-cloud-monitoring:jar:1.40.0
+  native.maven_jar(
+      name = "com_google_api_grpc_proto_google_cloud_monitoring_v3",
+      artifact = "com.google.api.grpc:proto-google-cloud-monitoring-v3:1.22.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "5b8746703e9d8f2937d4925a70b030cfc5bf00f6",
+  )
+
+
+  # com.google.auth:google-auth-library-oauth2-http:jar:0.9.0 wanted version 1.19.0
+  # com.google.cloud:google-cloud-core:jar:1.40.0
+  native.maven_jar(
+      name = "com_google_http_client_google_http_client",
+      artifact = "com.google.http-client:google-http-client:1.24.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "396eac8d3fb1332675f82b208f48a469d64f3b4a",
+  )
+
+
+  native.maven_jar(
+      name = "io_prometheus_simpleclient_httpserver",
+      artifact = "io.prometheus:simpleclient_httpserver:0.3.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "a2c1aeecac28f5bfa9a92a67b071d246ac00bbec",
+  )
+
+
+  # io.grpc:grpc-core:jar:1.9.0
+  native.maven_jar(
+      name = "com_google_instrumentation_instrumentation_api",
+      artifact = "com.google.instrumentation:instrumentation-api:0.4.3",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "41614af3429573dc02645d541638929d877945a2",
+  )
+
+
+  # com.google.auth:google-auth-library-oauth2-http:jar:0.9.0
+  native.maven_jar(
+      name = "com_google_http_client_google_http_client_jackson2",
+      artifact = "com.google.http-client:google-http-client-jackson2:1.19.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "81dbf9795d387d5e80e55346582d5f2fb81a42eb",
+  )
+
+
+  native.maven_jar(
+      name = "io_opencensus_opencensus_exporter_trace_logging",
+      artifact = "io.opencensus:opencensus-exporter-trace-logging:0.16.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "a3ca83ff7075c58e564aa029c35ccd8224616879",
+  )
+
+
+  # com.google.api:gax-grpc:jar:1.30.0 wanted version 1.13.1
+  # io.grpc:grpc-all:jar:1.9.0
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 1.13.1
+  # com.google.cloud:google-cloud-monitoring:jar:1.40.0 wanted version 1.13.1
+  # com.google.cloud:google-cloud-trace:jar:0.58.0-beta wanted version 1.13.1
+  native.maven_jar(
+      name = "io_grpc_grpc_auth",
+      artifact = "io.grpc:grpc-auth:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "d2eadc6d28ebee8ec0cef74f882255e4069972ad",
+  )
+
+
+  # com.google.cloud:google-cloud-core:jar:1.40.0
+  # com.google.api:gax-grpc:jar:1.30.0 got requested version
+  native.maven_jar(
+      name = "com_google_api_gax",
+      artifact = "com.google.api:gax:1.30.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "58fa2feb11b092be0a6ebe705a28736f12374230",
+  )
+
+
+  native.maven_jar(
+      name = "io_opencensus_opencensus_exporter_trace_stackdriver",
+      artifact = "io.opencensus:opencensus-exporter-trace-stackdriver:0.16.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "6ea1a99a5cc580f472fbddf34152b3dcd6929e88",
+  )
+
+
+  # com.google.guava:guava:bundle:23.0
+  native.maven_jar(
+      name = "com_google_j2objc_j2objc_annotations",
+      artifact = "com.google.j2objc:j2objc-annotations:1.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "ed28ded51a8b1c6b112568def5f4b455e6809019",
+  )
+
+
+  # io.grpc:grpc-auth:jar:1.9.0
+  # io.grpc:grpc-protobuf:jar:1.9.0 got requested version
+  # io.grpc:grpc-okhttp:jar:1.9.0 got requested version
+  # io.grpc:grpc-stub:jar:1.9.0 got requested version
+  # io.grpc:grpc-protobuf-lite:jar:1.9.0 got requested version
+  # io.grpc:grpc-all:jar:1.9.0 got requested version
+  # io.grpc:grpc-protobuf-nano:jar:1.9.0 got requested version
+  # io.grpc:grpc-testing:jar:1.9.0 got requested version
+  # io.grpc:grpc-netty:jar:1.9.0 got requested version
+  # io.grpc:grpc-netty-shaded:jar:1.13.1 wanted version 1.13.1
+  native.maven_jar(
+      name = "io_grpc_grpc_core",
+      artifact = "io.grpc:grpc-core:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "cf76ab13d35e8bd5d0ffad6d82bb1ef1770f050c",
+  )
+
+
+  # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1
+  # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1 got requested version
+  native.maven_jar(
+      name = "io_opencensus_opencensus_contrib_monitored_resource_util",
+      artifact = "io.opencensus:opencensus-contrib-monitored-resource-util:0.16.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "9edb4161978ac89f99a69544bfdc71b018a2509d",
+  )
+
+
+  # com.google.cloud:google-cloud-core:jar:1.40.0
+  native.maven_jar(
+      name = "joda_time_joda_time",
+      artifact = "joda-time:joda-time:2.9.2",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "36d6e77a419cb455e6fd5909f6f96b168e21e9d0",
+  )
+
+
+  # io.grpc:grpc-testing:jar:1.9.0
+  native.maven_jar(
+      name = "org_mockito_mockito_core",
+      artifact = "org.mockito:mockito-core:1.9.5",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "c3264abeea62c4d2f367e21484fbb40c7e256393",
+  )
+
+
+  # org.apache.httpcomponents:httpclient:jar:4.5.3
+  native.maven_jar(
+      name = "org_apache_httpcomponents_httpcore",
+      artifact = "org.apache.httpcomponents:httpcore:4.4.6",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "e3fd8ced1f52c7574af952e2e6da0df8df08eb82",
+  )
+
+
+  # io.opencensus:opencensus-impl:jar:0.16.1
+  native.maven_jar(
+      name = "com_lmax_disruptor",
+      artifact = "com.lmax:disruptor:3.4.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "72fabfe8a183f53bf61e0303921b7a89d2e8daed",
+  )
+
+
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 3.6.0
+  # com.google.api.grpc:proto-google-cloud-trace-v1:jar:0.23.0 wanted version 3.6.0
+  # com.google.api.grpc:proto-google-cloud-monitoring-v3:jar:1.22.0 wanted version 3.6.0
+  # io.grpc:grpc-protobuf:jar:1.9.0
+  # com.google.api.grpc:proto-google-iam-v1:jar:0.12.0 got requested version
+  # com.google.protobuf:protobuf-java-util:bundle:3.5.1 got requested version
+  # com.google.api.grpc:proto-google-cloud-trace-v2:jar:0.23.0 wanted version 3.6.0
+  native.maven_jar(
+      name = "com_google_protobuf_protobuf_java",
+      artifact = "com.google.protobuf:protobuf-java:3.5.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "8c3492f7662fa1cbf8ca76a0f5eb1146f7725acd",
+  )
+
+
+  # io.grpc:grpc-okhttp:jar:1.9.0
+  native.maven_jar(
+      name = "com_squareup_okhttp_okhttp",
+      artifact = "com.squareup.okhttp:okhttp:2.5.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "4de2b4ed3445c37ec1720a7d214712e845a24636",
+  )
+
+
+  # io.grpc:grpc-testing:jar:1.9.0 got requested version
+  # com.google.api:gax-grpc:jar:1.30.0 wanted version 1.13.1
+  # io.grpc:grpc-all:jar:1.9.0
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 1.13.1
+  # com.google.cloud:google-cloud-monitoring:jar:1.40.0 wanted version 1.13.1
+  # com.google.cloud:google-cloud-trace:jar:0.58.0-beta wanted version 1.13.1
+  native.maven_jar(
+      name = "io_grpc_grpc_stub",
+      artifact = "io.grpc:grpc-stub:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "20e310f888860a27dfa509a69eebb236417ee93f",
+  )
+
+
+  native.maven_jar(
+      name = "io_opencensus_opencensus_impl",
+      artifact = "io.opencensus:opencensus-impl:0.16.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "f9b06bf8422ba3700346173524087d005725432e",
+  )
+
+
+  # com.google.api:gax-grpc:jar:1.30.0 wanted version 1.13.1
+  # io.grpc:grpc-all:jar:1.9.0
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 1.13.1
+  native.maven_jar(
+      name = "io_grpc_grpc_protobuf",
+      artifact = "io.grpc:grpc-protobuf:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "94ca247577e4cf1a38d5ac9d536ac1d426a1ccc5",
+  )
+
+
+  # io.netty:netty-handler-proxy:jar:4.1.17.Final
+  native.maven_jar(
+      name = "io_netty_netty_codec_socks",
+      artifact = "io.netty:netty-codec-socks:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "a159bf1f3d5019e0d561c92fbbec8400967471fa",
+  )
+
+
+  # io.netty:netty-codec-http:jar:4.1.17.Final
+  # io.netty:netty-codec-socks:jar:4.1.17.Final got requested version
+  # io.netty:netty-handler:jar:4.1.17.Final got requested version
+  native.maven_jar(
+      name = "io_netty_netty_codec",
+      artifact = "io.netty:netty-codec:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "1d00f56dc9e55203a4bde5aae3d0828fdeb818e7",
+  )
+
+
+  # io.netty:netty-transport:jar:4.1.17.Final
+  # io.netty:netty-handler:jar:4.1.17.Final got requested version
+  native.maven_jar(
+      name = "io_netty_netty_buffer",
+      artifact = "io.netty:netty-buffer:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "fdd68fb3defd7059a7392b9395ee941ef9bacc25",
+  )
+
+
+  # com.google.cloud:google-cloud-trace:jar:0.58.0-beta got requested version
+  # com.google.cloud:google-cloud-monitoring:jar:1.40.0
+  native.maven_jar(
+      name = "com_google_cloud_google_cloud_core_grpc",
+      artifact = "com.google.cloud:google-cloud-core-grpc:1.40.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "f1f7a81915728eb53b9d3832f3ccec53ea181664",
+  )
+
+
+  # io.grpc:grpc-all:jar:1.9.0
+  native.maven_jar(
+      name = "io_grpc_grpc_netty",
+      artifact = "io.grpc:grpc-netty:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "8157384d87497dc18604a5ba3760763fe643f16e",
+  )
+
+
+  # io.grpc:grpc-all:jar:1.9.0
+  native.maven_jar(
+      name = "io_grpc_grpc_testing",
+      artifact = "io.grpc:grpc-testing:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "3d20675f0e64825f565a7d21456e7dbdd5886c6b",
+  )
+
+
+  # io.opencensus:opencensus-impl:jar:0.16.1 got requested version
+  # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1 got requested version
+  # io.opencensus:opencensus-exporter-trace-logging:jar:0.16.1 got requested version
+  # io.opencensus:opencensus-contrib-grpc-metrics:jar:0.10.0 wanted version 0.10.0
+  # io.opencensus:opencensus-exporter-stats-prometheus:jar:0.16.1 got requested version
+  # io.opencensus:opencensus-contrib-zpages:jar:0.16.1 got requested version
+  # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1 got requested version
+  # io.opencensus:opencensus-impl-core:jar:0.16.1 got requested version
+  native.maven_jar(
+      name = "io_opencensus_opencensus_api",
+      artifact = "io.opencensus:opencensus-api:0.16.1",
+      sha1 = "ec5d81a80d9c010c50368ad9045d512828d0d62d",
+  )
+
+
+  # io.grpc:grpc-testing:jar:1.9.0
+  native.maven_jar(
+      name = "junit_junit",
+      artifact = "junit:junit:4.12",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "2973d150c0dc1fefe998f834810d68f278ea58ec",
+  )
+
+
+  # io.prometheus:simpleclient_httpserver:bundle:0.4.0 wanted version 0.3.0
+  # io.prometheus:simpleclient_common:bundle:0.4.0 wanted version 0.3.0
+  # io.opencensus:opencensus-exporter-stats-prometheus:jar:0.16.1
+  native.maven_jar(
+      name = "io_prometheus_simpleclient",
+      artifact = "io.prometheus:simpleclient:0.4.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "99c293bbf9461587b2179273b6fdc349582a1021",
+  )
+
+
+  # com.google.guava:guava:bundle:23.0
+  native.maven_jar(
+      name = "org_codehaus_mojo_animal_sniffer_annotations",
+      artifact = "org.codehaus.mojo:animal-sniffer-annotations:1.14",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "775b7e22fb10026eed3f86e8dc556dfafe35f2d5",
+  )
+
+
+  native.maven_jar(
+      name = "io_opencensus_opencensus_exporter_stats_stackdriver",
+      artifact = "io.opencensus:opencensus-exporter-stats-stackdriver:0.16.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "e4e7152e53c7683e92a1ddae15a2e13eeaa7714e",
+  )
+
+
+  # io.netty:netty-handler-proxy:jar:4.1.17.Final got requested version
+  # io.netty:netty-codec-http2:jar:4.1.17.Final
+  native.maven_jar(
+      name = "io_netty_netty_codec_http",
+      artifact = "io.netty:netty-codec-http:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "251d7edcb897122b9b23f24ff793cd0739056b9e",
+  )
+
+
+  # org.apache.httpcomponents:httpclient:jar:4.5.3
+  native.maven_jar(
+      name = "commons_logging_commons_logging",
+      artifact = "commons-logging:commons-logging:1.2",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "4bfc12adfe4842bf07b657f0369c4cb522955686",
+  )
+
+
+  # io.grpc:grpc-netty:jar:1.9.0
+  native.maven_jar(
+      name = "io_netty_netty_codec_http2",
+      artifact = "io.netty:netty-codec-http2:4.1.17.Final",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "f9844005869c6d9049f4b677228a89fee4c6eab3",
+  )
+
+
+  # com.google.protobuf:protobuf-java-util:bundle:3.5.1
+  native.maven_jar(
+      name = "com_google_code_gson_gson",
+      artifact = "com.google.code.gson:gson:2.7",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "751f548c85fa49f330cecbb1875893f971b33c4e",
+  )
+
+
+  # io.grpc:grpc-protobuf-nano:jar:1.9.0
+  native.maven_jar(
+      name = "com_google_protobuf_nano_protobuf_javanano",
+      artifact = "com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-5",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "357e60f95cebb87c72151e49ba1f570d899734f8",
+  )
+
+
+  # com.google.http-client:google-http-client:jar:1.24.1
+  native.maven_jar(
+      name = "org_apache_httpcomponents_httpclient",
+      artifact = "org.apache.httpcomponents:httpclient:4.5.3",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "d1577ae15f01ef5438c5afc62162457c00a34713",
+  )
+
+
+  # com.google.cloud:google-cloud-core:jar:1.40.0
+  native.maven_jar(
+      name = "com_google_api_grpc_proto_google_iam_v1",
+      artifact = "com.google.api.grpc:proto-google-iam-v1:0.12.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "ea312c0250a5d0a7cdd1b20bc2c3259938b79855",
+  )
+
+
+  # io.opencensus:opencensus-api:jar:0.10.0 wanted version 1.8.0
+  # io.grpc:grpc-all:jar:1.9.0 got requested version
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 1.13.1
+  # io.grpc:grpc-core:jar:1.9.0
+  native.maven_jar(
+      name = "io_grpc_grpc_context",
+      artifact = "io.grpc:grpc-context:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "28b0836f48c9705abf73829bbc536dba29a1329a",
+  )
+
+
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0
+  native.maven_jar(
+      name = "com_google_api_gax_grpc",
+      artifact = "com.google.api:gax-grpc:1.30.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "ada82a4a0c020807e1c1a674b18658374264e401",
+  )
+
+
+  # com.google.api.grpc:proto-google-cloud-monitoring-v3:jar:1.22.0 wanted version 1.12.0
+  # com.google.api.grpc:proto-google-iam-v1:jar:0.12.0 wanted version 1.11.0
+  # com.google.api:gax-grpc:jar:1.30.0 wanted version 1.12.0
+  # com.google.api.grpc:proto-google-cloud-trace-v1:jar:0.23.0 wanted version 1.12.0
+  # io.grpc:grpc-protobuf:jar:1.9.0
+  # com.google.api.grpc:proto-google-cloud-trace-v2:jar:0.23.0 wanted version 1.12.0
+  # com.google.cloud:google-cloud-core:jar:1.40.0 wanted version 1.12.0
+  native.maven_jar(
+      name = "com_google_api_grpc_proto_google_common_protos",
+      artifact = "com.google.api.grpc:proto-google-common-protos:1.0.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "86f070507e28b930e50d218ee5b6788ef0dd05e6",
+  )
+
+
+  native.maven_jar(
+      name = "io_opencensus_opencensus_contrib_zpages",
+      artifact = "io.opencensus:opencensus-contrib-zpages:0.16.1",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "5fe09e41a9435281eb4547bc57ae34b9fd6bbf21",
+  )
+
+
+  # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1 wanted version 20.0
+  # io.opencensus:opencensus-exporter-stats-prometheus:jar:0.16.1 wanted version 20.0
+  # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1 wanted version 20.0
+  # io.grpc:grpc-protobuf-lite:jar:1.9.0 wanted version 19.0
+  # com.google.instrumentation:instrumentation-api:jar:0.4.3 wanted version 19.0
+  # io.grpc:grpc-protobuf:jar:1.9.0 wanted version 19.0
+  # io.opencensus:opencensus-contrib-zpages:jar:0.16.1 wanted version 20.0
+  # io.opencensus:opencensus-impl-core:jar:0.16.1 wanted version 20.0
+  # io.opencensus:opencensus-exporter-trace-logging:jar:0.16.1 wanted version 20.0
+  # io.grpc:grpc-protobuf-nano:jar:1.9.0 wanted version 19.0
+  # io.grpc:grpc-core:jar:1.9.0 wanted version 19.0
+  # com.google.protobuf:protobuf-java-util:bundle:3.5.1 wanted version 19.0
+  # io.opencensus:opencensus-api:jar:0.10.0 wanted version 19.0
+  native.maven_jar(
+      name = "com_google_guava_guava",
+      artifact = "com.google.guava:guava:23.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "c947004bb13d18182be60077ade044099e4f26f1",
+  )
+
+
+  native.maven_jar(
+      name = "io_grpc_grpc_all",
+      artifact = "io.grpc:grpc-all:1.9.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "442dfac27fd072e15b7134ab02c2b38136036090",
+  )
+
+
+  # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 got requested version
+  # com.google.cloud:google-cloud-trace:jar:0.58.0-beta got requested version
+  # com.google.cloud:google-cloud-monitoring:jar:1.40.0
+  native.maven_jar(
+      name = "com_google_cloud_google_cloud_core",
+      artifact = "com.google.cloud:google-cloud-core:1.40.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "4985701f989030e262cf8f4e38cc954115f5b082",
+  )
+
+
+  # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1
+  native.maven_jar(
+      name = "com_google_cloud_google_cloud_monitoring",
+      artifact = "com.google.cloud:google-cloud-monitoring:1.40.0",
+      repository = "http://repo.maven.apache.org/maven2/",
+      sha1 = "f03d20d67a5f3b0cd0685225a6ea5339d208aa53",
+  )
+
+
+
+
+def opencensus_java_libraries():
+  native.java_library(
+      name = "com_google_code_findbugs_jsr305",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_code_findbugs_jsr305//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_protobuf_lite",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_protobuf_lite//jar"],
+      runtime_deps = [
+          ":com_google_guava_guava",
+          ":io_grpc_grpc_core",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_exporter_stats_prometheus",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_exporter_stats_prometheus//jar"],
+      runtime_deps = [
+          ":com_google_guava_guava",
+          ":io_opencensus_opencensus_api",
+          ":io_prometheus_simpleclient",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_auth_google_auth_library_oauth2_http",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_auth_google_auth_library_oauth2_http//jar"],
+      runtime_deps = [
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_http_client_google_http_client",
+          ":com_google_http_client_google_http_client_jackson2",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_transport",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_transport//jar"],
+      runtime_deps = [
+          ":io_netty_netty_buffer",
+          ":io_netty_netty_common",
+          ":io_netty_netty_resolver",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_handler_proxy",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_handler_proxy//jar"],
+      runtime_deps = [
+          ":io_netty_netty_codec",
+          ":io_netty_netty_codec_http",
+          ":io_netty_netty_codec_socks",
+          ":io_netty_netty_transport",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_protobuf_nano",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_protobuf_nano//jar"],
+      runtime_deps = [
+          ":com_google_guava_guava",
+          ":com_google_protobuf_nano_protobuf_javanano",
+          ":io_grpc_grpc_core",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_cloud_google_cloud_trace",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_cloud_google_cloud_trace//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_grpc_proto_google_cloud_trace_v1",
+          ":com_google_api_grpc_proto_google_cloud_trace_v2",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_cloud_google_cloud_core",
+          ":com_google_cloud_google_cloud_core_grpc",
+          ":com_google_protobuf_protobuf_java",
+          ":io_grpc_grpc_auth",
+          ":io_grpc_grpc_netty_shaded",
+          ":io_grpc_grpc_stub",
+      ],
+  )
+
+
+  native.java_library(
+      name = "commons_codec_commons_codec",
+      visibility = ["//visibility:public"],
+      exports = ["@commons_codec_commons_codec//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_impl_core",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_impl_core//jar"],
+      runtime_deps = [
+          ":com_google_guava_guava",
+          ":io_opencensus_opencensus_api",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_prometheus_simpleclient_common",
+      visibility = ["//visibility:public"],
+      exports = ["@io_prometheus_simpleclient_common//jar"],
+      runtime_deps = [
+          ":io_prometheus_simpleclient",
+      ],
+  )
+
+
+  native.java_library(
+      name = "org_threeten_threetenbp",
+      visibility = ["//visibility:public"],
+      exports = ["@org_threeten_threetenbp//jar"],
+  )
+
+
+  native.java_library(
+      name = "com_google_errorprone_error_prone_annotations",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_errorprone_error_prone_annotations//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_resolver",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_resolver//jar"],
+      runtime_deps = [
+          ":io_netty_netty_common",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_squareup_okio_okio",
+      visibility = ["//visibility:public"],
+      exports = ["@com_squareup_okio_okio//jar"],
+  )
+
+
+  native.java_library(
+      name = "com_google_protobuf_protobuf_java_util",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_protobuf_protobuf_java_util//jar"],
+      runtime_deps = [
+          ":com_google_code_gson_gson",
+          ":com_google_guava_guava",
+          ":com_google_protobuf_protobuf_java",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_auth_google_auth_library_credentials",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_auth_google_auth_library_credentials//jar"],
+  )
+
+
+  native.java_library(
+      name = "com_google_api_api_common",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_api_api_common//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_contrib_grpc_metrics",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_contrib_grpc_metrics//jar"],
+      runtime_deps = [
+          ":com_google_code_findbugs_jsr305",
+          ":com_google_errorprone_error_prone_annotations",
+          ":io_opencensus_opencensus_api",
+      ],
+  )
+
+
+  native.java_library(
+      name = "org_objenesis_objenesis",
+      visibility = ["//visibility:public"],
+      exports = ["@org_objenesis_objenesis//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_common",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_common//jar"],
+  )
+
+
+  native.java_library(
+      name = "com_google_api_grpc_proto_google_cloud_trace_v2",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_api_grpc_proto_google_cloud_trace_v2//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_protobuf_protobuf_java",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_netty_shaded",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_netty_shaded//jar"],
+      runtime_deps = [
+          ":io_grpc_grpc_core",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_api_grpc_proto_google_cloud_trace_v1",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_api_grpc_proto_google_cloud_trace_v1//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_protobuf_protobuf_java",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_okhttp",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_okhttp//jar"],
+      runtime_deps = [
+          ":com_squareup_okhttp_okhttp",
+          ":com_squareup_okio_okio",
+          ":io_grpc_grpc_core",
+      ],
+  )
+
+
+  native.java_library(
+      name = "org_hamcrest_hamcrest_core",
+      visibility = ["//visibility:public"],
+      exports = ["@org_hamcrest_hamcrest_core//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_handler",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_handler//jar"],
+      runtime_deps = [
+          ":io_netty_netty_buffer",
+          ":io_netty_netty_codec",
+          ":io_netty_netty_transport",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_api_grpc_proto_google_cloud_monitoring_v3",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_api_grpc_proto_google_cloud_monitoring_v3//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_protobuf_protobuf_java",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_http_client_google_http_client",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_http_client_google_http_client//jar"],
+      runtime_deps = [
+          ":commons_codec_commons_codec",
+          ":commons_logging_commons_logging",
+          ":org_apache_httpcomponents_httpclient",
+          ":org_apache_httpcomponents_httpcore",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_prometheus_simpleclient_httpserver",
+      visibility = ["//visibility:public"],
+      exports = ["@io_prometheus_simpleclient_httpserver//jar"],
+      runtime_deps = [
+          ":io_prometheus_simpleclient",
+          ":io_prometheus_simpleclient_common",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_instrumentation_instrumentation_api",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_instrumentation_instrumentation_api//jar"],
+      runtime_deps = [
+          ":com_google_code_findbugs_jsr305",
+          ":com_google_guava_guava",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_http_client_google_http_client_jackson2",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_http_client_google_http_client_jackson2//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_exporter_trace_logging",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_exporter_trace_logging//jar"],
+      runtime_deps = [
+          ":com_google_guava_guava",
+          ":io_opencensus_opencensus_api",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_auth",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_auth//jar"],
+      runtime_deps = [
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_code_findbugs_jsr305",
+          ":com_google_errorprone_error_prone_annotations",
+          ":com_google_guava_guava",
+          ":com_google_instrumentation_instrumentation_api",
+          ":io_grpc_grpc_context",
+          ":io_grpc_grpc_core",
+          ":io_opencensus_opencensus_api",
+          ":io_opencensus_opencensus_contrib_grpc_metrics",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_api_gax",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_api_gax//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_auth_google_auth_library_oauth2_http",
+          ":com_google_http_client_google_http_client",
+          ":com_google_http_client_google_http_client_jackson2",
+          ":org_threeten_threetenbp",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_exporter_trace_stackdriver",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_exporter_trace_stackdriver//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_grpc_proto_google_cloud_trace_v1",
+          ":com_google_api_grpc_proto_google_cloud_trace_v2",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_cloud_google_cloud_core",
+          ":com_google_cloud_google_cloud_core_grpc",
+          ":com_google_cloud_google_cloud_trace",
+          ":com_google_guava_guava",
+          ":com_google_protobuf_protobuf_java",
+          ":io_grpc_grpc_auth",
+          ":io_grpc_grpc_netty_shaded",
+          ":io_grpc_grpc_stub",
+          ":io_opencensus_opencensus_api",
+          ":io_opencensus_opencensus_contrib_monitored_resource_util",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_j2objc_j2objc_annotations",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_j2objc_j2objc_annotations//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_core",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_core//jar"],
+      runtime_deps = [
+          ":com_google_code_findbugs_jsr305",
+          ":com_google_errorprone_error_prone_annotations",
+          ":com_google_guava_guava",
+          ":com_google_instrumentation_instrumentation_api",
+          ":io_grpc_grpc_context",
+          ":io_opencensus_opencensus_api",
+          ":io_opencensus_opencensus_contrib_grpc_metrics",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_contrib_monitored_resource_util",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_contrib_monitored_resource_util//jar"],
+  )
+
+
+  native.java_library(
+      name = "joda_time_joda_time",
+      visibility = ["//visibility:public"],
+      exports = ["@joda_time_joda_time//jar"],
+  )
+
+
+  native.java_library(
+      name = "org_mockito_mockito_core",
+      visibility = ["//visibility:public"],
+      exports = ["@org_mockito_mockito_core//jar"],
+      runtime_deps = [
+          ":org_objenesis_objenesis",
+      ],
+  )
+
+
+  native.java_library(
+      name = "org_apache_httpcomponents_httpcore",
+      visibility = ["//visibility:public"],
+      exports = ["@org_apache_httpcomponents_httpcore//jar"],
+  )
+
+
+  native.java_library(
+      name = "com_lmax_disruptor",
+      visibility = ["//visibility:public"],
+      exports = ["@com_lmax_disruptor//jar"],
+  )
+
+
+  native.java_library(
+      name = "com_google_protobuf_protobuf_java",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_protobuf_protobuf_java//jar"],
+  )
+
+
+  native.java_library(
+      name = "com_squareup_okhttp_okhttp",
+      visibility = ["//visibility:public"],
+      exports = ["@com_squareup_okhttp_okhttp//jar"],
+      runtime_deps = [
+          ":com_squareup_okio_okio",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_stub",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_stub//jar"],
+      runtime_deps = [
+          ":io_grpc_grpc_core",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_impl",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_impl//jar"],
+      runtime_deps = [
+          ":com_google_guava_guava",
+          ":com_lmax_disruptor",
+          ":io_opencensus_opencensus_api",
+          ":io_opencensus_opencensus_impl_core",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_protobuf",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_protobuf//jar"],
+      runtime_deps = [
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_code_gson_gson",
+          ":com_google_guava_guava",
+          ":com_google_protobuf_protobuf_java",
+          ":com_google_protobuf_protobuf_java_util",
+          ":io_grpc_grpc_core",
+          ":io_grpc_grpc_protobuf_lite",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_codec_socks",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_codec_socks//jar"],
+      runtime_deps = [
+          ":io_netty_netty_codec",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_codec",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_codec//jar"],
+      runtime_deps = [
+          ":io_netty_netty_buffer",
+          ":io_netty_netty_common",
+          ":io_netty_netty_resolver",
+          ":io_netty_netty_transport",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_buffer",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_buffer//jar"],
+      runtime_deps = [
+          ":io_netty_netty_common",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_cloud_google_cloud_core_grpc",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_cloud_google_cloud_core_grpc//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_gax",
+          ":com_google_api_gax_grpc",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_auth_google_auth_library_oauth2_http",
+          ":com_google_cloud_google_cloud_core",
+          ":com_google_protobuf_protobuf_java",
+          ":com_google_protobuf_protobuf_java_util",
+          ":io_grpc_grpc_auth",
+          ":io_grpc_grpc_context",
+          ":io_grpc_grpc_core",
+          ":io_grpc_grpc_netty_shaded",
+          ":io_grpc_grpc_protobuf",
+          ":io_grpc_grpc_stub",
+          ":org_threeten_threetenbp",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_netty",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_netty//jar"],
+      runtime_deps = [
+          ":io_grpc_grpc_core",
+          ":io_netty_netty_buffer",
+          ":io_netty_netty_codec",
+          ":io_netty_netty_codec_http",
+          ":io_netty_netty_codec_http2",
+          ":io_netty_netty_codec_socks",
+          ":io_netty_netty_common",
+          ":io_netty_netty_handler",
+          ":io_netty_netty_handler_proxy",
+          ":io_netty_netty_resolver",
+          ":io_netty_netty_transport",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_testing",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_testing//jar"],
+      runtime_deps = [
+          ":io_grpc_grpc_core",
+          ":io_grpc_grpc_stub",
+          ":junit_junit",
+          ":org_hamcrest_hamcrest_core",
+          ":org_mockito_mockito_core",
+          ":org_objenesis_objenesis",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_api",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_api//jar"],
+      runtime_deps = [
+          ":com_google_code_findbugs_jsr305",
+          ":com_google_errorprone_error_prone_annotations",
+          ":com_google_guava_guava",
+          ":io_grpc_grpc_context",
+      ],
+  )
+
+
+  native.java_library(
+      name = "junit_junit",
+      visibility = ["//visibility:public"],
+      exports = ["@junit_junit//jar"],
+      runtime_deps = [
+          ":org_hamcrest_hamcrest_core",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_prometheus_simpleclient",
+      visibility = ["//visibility:public"],
+      exports = ["@io_prometheus_simpleclient//jar"],
+  )
+
+
+  native.java_library(
+      name = "org_codehaus_mojo_animal_sniffer_annotations",
+      visibility = ["//visibility:public"],
+      exports = ["@org_codehaus_mojo_animal_sniffer_annotations//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_exporter_stats_stackdriver",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_exporter_stats_stackdriver//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_gax",
+          ":com_google_api_gax_grpc",
+          ":com_google_api_grpc_proto_google_cloud_monitoring_v3",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_api_grpc_proto_google_iam_v1",
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_auth_google_auth_library_oauth2_http",
+          ":com_google_cloud_google_cloud_core",
+          ":com_google_cloud_google_cloud_core_grpc",
+          ":com_google_cloud_google_cloud_monitoring",
+          ":com_google_guava_guava",
+          ":com_google_http_client_google_http_client",
+          ":com_google_http_client_google_http_client_jackson2",
+          ":com_google_protobuf_protobuf_java",
+          ":com_google_protobuf_protobuf_java_util",
+          ":commons_codec_commons_codec",
+          ":commons_logging_commons_logging",
+          ":io_grpc_grpc_auth",
+          ":io_grpc_grpc_context",
+          ":io_grpc_grpc_core",
+          ":io_grpc_grpc_netty_shaded",
+          ":io_grpc_grpc_protobuf",
+          ":io_grpc_grpc_stub",
+          ":io_opencensus_opencensus_api",
+          ":io_opencensus_opencensus_contrib_monitored_resource_util",
+          ":joda_time_joda_time",
+          ":org_apache_httpcomponents_httpclient",
+          ":org_apache_httpcomponents_httpcore",
+          ":org_threeten_threetenbp",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_codec_http",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_codec_http//jar"],
+      runtime_deps = [
+          ":io_netty_netty_buffer",
+          ":io_netty_netty_codec",
+          ":io_netty_netty_common",
+          ":io_netty_netty_resolver",
+          ":io_netty_netty_transport",
+      ],
+  )
+
+
+  native.java_library(
+      name = "commons_logging_commons_logging",
+      visibility = ["//visibility:public"],
+      exports = ["@commons_logging_commons_logging//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_netty_netty_codec_http2",
+      visibility = ["//visibility:public"],
+      exports = ["@io_netty_netty_codec_http2//jar"],
+      runtime_deps = [
+          ":io_netty_netty_buffer",
+          ":io_netty_netty_codec",
+          ":io_netty_netty_codec_http",
+          ":io_netty_netty_common",
+          ":io_netty_netty_handler",
+          ":io_netty_netty_resolver",
+          ":io_netty_netty_transport",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_code_gson_gson",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_code_gson_gson//jar"],
+  )
+
+
+  native.java_library(
+      name = "com_google_protobuf_nano_protobuf_javanano",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_protobuf_nano_protobuf_javanano//jar"],
+  )
+
+
+  native.java_library(
+      name = "org_apache_httpcomponents_httpclient",
+      visibility = ["//visibility:public"],
+      exports = ["@org_apache_httpcomponents_httpclient//jar"],
+      runtime_deps = [
+          ":commons_codec_commons_codec",
+          ":commons_logging_commons_logging",
+          ":org_apache_httpcomponents_httpcore",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_api_grpc_proto_google_iam_v1",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_api_grpc_proto_google_iam_v1//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_protobuf_protobuf_java",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_context",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_context//jar"],
+  )
+
+
+  native.java_library(
+      name = "com_google_api_gax_grpc",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_api_gax_grpc//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_gax",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_auth_google_auth_library_oauth2_http",
+          ":io_grpc_grpc_auth",
+          ":io_grpc_grpc_protobuf",
+          ":io_grpc_grpc_stub",
+          ":org_threeten_threetenbp",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_api_grpc_proto_google_common_protos",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_api_grpc_proto_google_common_protos//jar"],
+  )
+
+
+  native.java_library(
+      name = "io_opencensus_opencensus_contrib_zpages",
+      visibility = ["//visibility:public"],
+      exports = ["@io_opencensus_opencensus_contrib_zpages//jar"],
+      runtime_deps = [
+          ":com_google_guava_guava",
+          ":io_opencensus_opencensus_api",
+          ":io_opencensus_opencensus_contrib_grpc_metrics",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_guava_guava",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_guava_guava//jar"],
+      runtime_deps = [
+          ":com_google_code_findbugs_jsr305",
+          ":com_google_errorprone_error_prone_annotations",
+          ":com_google_j2objc_j2objc_annotations",
+          ":org_codehaus_mojo_animal_sniffer_annotations",
+      ],
+  )
+
+
+  native.java_library(
+      name = "io_grpc_grpc_all",
+      visibility = ["//visibility:public"],
+      exports = ["@io_grpc_grpc_all//jar"],
+      runtime_deps = [
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_code_findbugs_jsr305",
+          ":com_google_code_gson_gson",
+          ":com_google_errorprone_error_prone_annotations",
+          ":com_google_guava_guava",
+          ":com_google_instrumentation_instrumentation_api",
+          ":com_google_protobuf_nano_protobuf_javanano",
+          ":com_google_protobuf_protobuf_java",
+          ":com_google_protobuf_protobuf_java_util",
+          ":com_squareup_okhttp_okhttp",
+          ":com_squareup_okio_okio",
+          ":io_grpc_grpc_auth",
+          ":io_grpc_grpc_context",
+          ":io_grpc_grpc_core",
+          ":io_grpc_grpc_netty",
+          ":io_grpc_grpc_okhttp",
+          ":io_grpc_grpc_protobuf",
+          ":io_grpc_grpc_protobuf_lite",
+          ":io_grpc_grpc_protobuf_nano",
+          ":io_grpc_grpc_stub",
+          ":io_grpc_grpc_testing",
+          ":io_netty_netty_buffer",
+          ":io_netty_netty_codec",
+          ":io_netty_netty_codec_http",
+          ":io_netty_netty_codec_http2",
+          ":io_netty_netty_codec_socks",
+          ":io_netty_netty_common",
+          ":io_netty_netty_handler",
+          ":io_netty_netty_handler_proxy",
+          ":io_netty_netty_resolver",
+          ":io_netty_netty_transport",
+          ":io_opencensus_opencensus_api",
+          ":io_opencensus_opencensus_contrib_grpc_metrics",
+          ":junit_junit",
+          ":org_hamcrest_hamcrest_core",
+          ":org_mockito_mockito_core",
+          ":org_objenesis_objenesis",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_cloud_google_cloud_core",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_cloud_google_cloud_core//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_gax",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_api_grpc_proto_google_iam_v1",
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_auth_google_auth_library_oauth2_http",
+          ":com_google_http_client_google_http_client",
+          ":com_google_http_client_google_http_client_jackson2",
+          ":com_google_protobuf_protobuf_java",
+          ":com_google_protobuf_protobuf_java_util",
+          ":commons_codec_commons_codec",
+          ":commons_logging_commons_logging",
+          ":joda_time_joda_time",
+          ":org_apache_httpcomponents_httpclient",
+          ":org_apache_httpcomponents_httpcore",
+          ":org_threeten_threetenbp",
+      ],
+  )
+
+
+  native.java_library(
+      name = "com_google_cloud_google_cloud_monitoring",
+      visibility = ["//visibility:public"],
+      exports = ["@com_google_cloud_google_cloud_monitoring//jar"],
+      runtime_deps = [
+          ":com_google_api_api_common",
+          ":com_google_api_gax",
+          ":com_google_api_gax_grpc",
+          ":com_google_api_grpc_proto_google_cloud_monitoring_v3",
+          ":com_google_api_grpc_proto_google_common_protos",
+          ":com_google_api_grpc_proto_google_iam_v1",
+          ":com_google_auth_google_auth_library_credentials",
+          ":com_google_auth_google_auth_library_oauth2_http",
+          ":com_google_cloud_google_cloud_core",
+          ":com_google_cloud_google_cloud_core_grpc",
+          ":com_google_http_client_google_http_client",
+          ":com_google_http_client_google_http_client_jackson2",
+          ":com_google_protobuf_protobuf_java",
+          ":com_google_protobuf_protobuf_java_util",
+          ":commons_codec_commons_codec",
+          ":commons_logging_commons_logging",
+          ":io_grpc_grpc_auth",
+          ":io_grpc_grpc_context",
+          ":io_grpc_grpc_core",
+          ":io_grpc_grpc_netty_shaded",
+          ":io_grpc_grpc_protobuf",
+          ":io_grpc_grpc_stub",
+          ":joda_time_joda_time",
+          ":org_apache_httpcomponents_httpclient",
+          ":org_apache_httpcomponents_httpcore",
+          ":org_threeten_threetenbp",
+      ],
+  )
+
+
diff --git a/examples/pom.xml b/examples/pom.xml
new file mode 100644
index 0000000..5f08312
--- /dev/null
+++ b/examples/pom.xml
@@ -0,0 +1,169 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>io.opencensus</groupId>
+  <artifactId>opencensus-examples</artifactId>
+  <packaging>jar</packaging>
+  <version>0.17.0-SNAPSHOT</version><!-- CURRENT_OPENCENSUS_VERSION -->
+  <name>opencensus-examples</name>
+  <url>http://maven.apache.org</url>
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <!-- change to the version you want to use. -->
+    <opencensus.version>0.16.1</opencensus.version><!-- LATEST_OPENCENSUS_RELEASE_VERSION -->
+    <grpc.version>1.13.1</grpc.version><!-- CURRENT_GRPC_VERSION -->
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-api</artifactId>
+      <version>${opencensus.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-contrib-grpc-metrics</artifactId>
+      <version>${opencensus.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-contrib-zpages</artifactId>
+      <version>${opencensus.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-exporter-stats-stackdriver</artifactId>
+      <version>${opencensus.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-exporter-stats-prometheus</artifactId>
+      <version>${opencensus.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-exporter-trace-stackdriver</artifactId>
+      <version>${opencensus.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-exporter-trace-logging</artifactId>
+      <version>${opencensus.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.grpc</groupId>
+      <artifactId>grpc-netty</artifactId>
+      <version>${grpc.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.grpc</groupId>
+      <artifactId>grpc-protobuf</artifactId>
+      <version>${grpc.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.grpc</groupId>
+      <artifactId>grpc-stub</artifactId>
+      <version>${grpc.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.prometheus</groupId>
+      <artifactId>simpleclient_httpserver</artifactId>
+      <version>0.3.0</version>
+    </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-impl</artifactId>
+      <version>${opencensus.version}</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-tcnative-boringssl-static</artifactId>
+      <version>2.0.8.Final</version>
+      <scope>runtime</scope>
+    </dependency>
+  </dependencies>
+  <build>
+    <extensions>
+      <extension>
+        <groupId>kr.motd.maven</groupId>
+        <artifactId>os-maven-plugin</artifactId>
+        <version>1.5.0.Final</version>
+      </extension>
+    </extensions>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>3.7.0</version>
+          <configuration>
+            <source>1.8</source>
+            <target>1.8</target>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>appassembler-maven-plugin</artifactId>
+        <version>1.10</version>
+        <configuration>
+          <programs>
+            <program>
+              <id>TagContextExample</id>
+              <mainClass>io.opencensus.examples.tags.TagContextExample</mainClass>
+            </program>
+            <program>
+              <id>MultiSpansTracing</id>
+              <mainClass>io.opencensus.examples.trace.MultiSpansTracing</mainClass>
+            </program>
+            <program>
+              <id>MultiSpansScopedTracing</id>
+              <mainClass>io.opencensus.examples.trace.MultiSpansScopedTracing</mainClass>
+            </program>
+            <program>
+              <id>MultiSpansContextTracing</id>
+              <mainClass>io.opencensus.examples.trace.MultiSpansContextTracing</mainClass>
+            </program>
+            <program>
+              <id>ZPagesTester</id>
+              <mainClass>io.opencensus.examples.zpages.ZPagesTester</mainClass>
+            </program>
+            <program>
+              <id>QuickStart</id>
+              <mainClass>io.opencensus.examples.helloworld.QuickStart</mainClass>
+            </program>
+            <program>
+              <id>HelloWorldClient</id>
+              <mainClass>io.opencensus.examples.grpc.helloworld.HelloWorldClient</mainClass>
+            </program>
+            <program>
+              <id>HelloWorldServer</id>
+              <mainClass>io.opencensus.examples.grpc.helloworld.HelloWorldServer</mainClass>
+            </program>
+          </programs>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.xolstice.maven.plugins</groupId>
+        <artifactId>protobuf-maven-plugin</artifactId>
+        <version>0.5.0</version>
+        <configuration>
+          <protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact>
+          <pluginId>grpc-java</pluginId>
+          <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>compile</goal>
+              <goal>compile-custom</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
+
diff --git a/examples/settings.gradle b/examples/settings.gradle
new file mode 100644
index 0000000..310e652
--- /dev/null
+++ b/examples/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'opencensus-examples'
diff --git a/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldClient.java b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldClient.java
new file mode 100644
index 0000000..30e4163
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldClient.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018, 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.examples.grpc.helloworld;
+
+import static io.opencensus.examples.grpc.helloworld.HelloWorldUtils.getPortOrDefaultFromArgs;
+import static io.opencensus.examples.grpc.helloworld.HelloWorldUtils.getStringOrDefaultFromArgs;
+
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.StatusRuntimeException;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.grpc.metrics.RpcViews;
+import io.opencensus.contrib.zpages.ZPageHandlers;
+import io.opencensus.exporter.stats.prometheus.PrometheusStatsCollector;
+import io.opencensus.exporter.stats.stackdriver.StackdriverStatsConfiguration;
+import io.opencensus.exporter.stats.stackdriver.StackdriverStatsExporter;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.exporter.trace.stackdriver.StackdriverTraceConfiguration;
+import io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** A simple client that requests a greeting from the {@link HelloWorldServer}. */
+public class HelloWorldClient {
+  private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName());
+
+  private static final Tracer tracer = Tracing.getTracer();
+
+  private final ManagedChannel channel;
+  private final GreeterGrpc.GreeterBlockingStub blockingStub;
+
+  /** Construct client connecting to HelloWorld server at {@code host:port}. */
+  public HelloWorldClient(String host, int port) {
+    this(
+        ManagedChannelBuilder.forAddress(host, port)
+            // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
+            // needing certificates.
+            .usePlaintext(true)
+            .build());
+  }
+
+  /** Construct client for accessing RouteGuide server using the existing channel. */
+  HelloWorldClient(ManagedChannel channel) {
+    this.channel = channel;
+    blockingStub = GreeterGrpc.newBlockingStub(channel);
+  }
+
+  public void shutdown() throws InterruptedException {
+    channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
+  }
+
+  /** Say hello to server. */
+  public void greet(String name) {
+    logger.info("Will try to greet " + name + " ...");
+    HelloRequest request = HelloRequest.newBuilder().setName(name).build();
+    HelloReply response;
+
+    SpanBuilder spanBuilder =
+        tracer.spanBuilder("client").setRecordEvents(true).setSampler(Samplers.alwaysSample());
+    try (Scope scope = spanBuilder.startScopedSpan()) {
+      tracer.getCurrentSpan().addAnnotation("Saying Hello to Server.");
+      response = blockingStub.sayHello(request);
+      tracer.getCurrentSpan().addAnnotation("Received response from Server.");
+    } catch (StatusRuntimeException e) {
+      tracer
+          .getCurrentSpan()
+          .setStatus(
+              CanonicalCode.valueOf(e.getStatus().getCode().name())
+                  .toStatus()
+                  .withDescription(e.getMessage()));
+      logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
+      return;
+    }
+    logger.info("Greeting: " + response.getMessage());
+  }
+
+  /**
+   * Greet server. If provided, the first element of {@code args} is the name to use in the
+   * greeting.
+   */
+  public static void main(String[] args) throws IOException, InterruptedException {
+    // Add final keyword to pass checkStyle.
+    final String user = getStringOrDefaultFromArgs(args, 0, "world");
+    final String host = getStringOrDefaultFromArgs(args, 1, "localhost");
+    final int serverPort = getPortOrDefaultFromArgs(args, 2, 50051);
+    final String cloudProjectId = getStringOrDefaultFromArgs(args, 3, null);
+    final int zPagePort = getPortOrDefaultFromArgs(args, 4, 3001);
+
+    // Registers all RPC views.
+    RpcViews.registerAllViews();
+
+    // Starts a HTTP server and registers all Zpages to it.
+    ZPageHandlers.startHttpServerAndRegisterAll(zPagePort);
+    logger.info("ZPages server starts at localhost:" + zPagePort);
+
+    // Registers logging trace exporter.
+    LoggingTraceExporter.register();
+
+    // Registers Stackdriver exporters.
+    if (cloudProjectId != null) {
+      StackdriverTraceExporter.createAndRegister(
+          StackdriverTraceConfiguration.builder().setProjectId(cloudProjectId).build());
+      StackdriverStatsExporter.createAndRegister(
+          StackdriverStatsConfiguration.builder()
+              .setProjectId(cloudProjectId)
+              .setExportInterval(Duration.create(15, 0))
+              .build());
+    }
+
+    // Register Prometheus exporters and export metrics to a Prometheus HTTPServer.
+    PrometheusStatsCollector.createAndRegister();
+
+    HelloWorldClient client = new HelloWorldClient(host, serverPort);
+    try {
+      client.greet(user);
+    } finally {
+      client.shutdown();
+    }
+
+    logger.info("Client sleeping, ^C to exit. Meanwhile you can view stats and spans on zpages.");
+    while (true) {
+      try {
+        Thread.sleep(10000);
+      } catch (InterruptedException e) {
+        logger.info("Exiting HelloWorldClient...");
+      }
+    }
+  }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldServer.java b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldServer.java
new file mode 100644
index 0000000..15a0a89
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldServer.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2018, 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.examples.grpc.helloworld;
+
+import static io.opencensus.examples.grpc.helloworld.HelloWorldUtils.getPortOrDefaultFromArgs;
+import static io.opencensus.examples.grpc.helloworld.HelloWorldUtils.getStringOrDefaultFromArgs;
+
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
+import io.grpc.stub.StreamObserver;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.grpc.metrics.RpcViews;
+import io.opencensus.contrib.zpages.ZPageHandlers;
+import io.opencensus.exporter.stats.prometheus.PrometheusStatsCollector;
+import io.opencensus.exporter.stats.stackdriver.StackdriverStatsConfiguration;
+import io.opencensus.exporter.stats.stackdriver.StackdriverStatsExporter;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.exporter.trace.stackdriver.StackdriverTraceConfiguration;
+import io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import io.prometheus.client.exporter.HTTPServer;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/** Server that manages startup/shutdown of a {@code Greeter} server. */
+public class HelloWorldServer {
+  private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName());
+
+  private static final Tracer tracer = Tracing.getTracer();
+
+  private final int serverPort;
+  private Server server;
+
+  private HelloWorldServer(int serverPort) {
+    this.serverPort = serverPort;
+  }
+
+  // A helper function that performs some work in its own Span.
+  private static void performWork(Span parent) {
+    SpanBuilder spanBuilder =
+        tracer
+            .spanBuilderWithExplicitParent("internal_work", parent)
+            .setRecordEvents(true)
+            .setSampler(Samplers.alwaysSample());
+    try (Scope scope = spanBuilder.startScopedSpan()) {
+      Span span = tracer.getCurrentSpan();
+      span.putAttribute("my_attribute", AttributeValue.stringAttributeValue("blue"));
+      span.addAnnotation("Performing work.");
+      sleepFor(20); // Working hard here.
+      span.addAnnotation("Done work.");
+    }
+  }
+
+  private static void sleepFor(int milliseconds) {
+    try {
+      Thread.sleep(milliseconds);
+    } catch (InterruptedException e) {
+      Span span = tracer.getCurrentSpan();
+      span.addAnnotation("Exception thrown when performing work " + e.getMessage());
+      span.setStatus(Status.UNKNOWN);
+    }
+  }
+
+  private void start() throws IOException {
+    server = ServerBuilder.forPort(serverPort).addService(new GreeterImpl()).build().start();
+    logger.info("Server started, listening on " + serverPort);
+    Runtime.getRuntime()
+        .addShutdownHook(
+            new Thread() {
+              @Override
+              public void run() {
+                // Use stderr here since the logger may have been reset by its JVM shutdown hook.
+                System.err.println("*** shutting down gRPC server since JVM is shutting down");
+                HelloWorldServer.this.stop();
+                System.err.println("*** server shut down");
+              }
+            });
+  }
+
+  private void stop() {
+    if (server != null) {
+      server.shutdown();
+    }
+  }
+
+  /** Await termination on the main thread since the grpc library uses daemon threads. */
+  private void blockUntilShutdown() throws InterruptedException {
+    if (server != null) {
+      server.awaitTermination();
+    }
+  }
+
+  /** Main launches the server from the command line. */
+  public static void main(String[] args) throws IOException, InterruptedException {
+    // Add final keyword to pass checkStyle.
+    final int serverPort = getPortOrDefaultFromArgs(args, 0, 50051);
+    final String cloudProjectId = getStringOrDefaultFromArgs(args, 1, null);
+    final int zPagePort = getPortOrDefaultFromArgs(args, 2, 3000);
+    final int prometheusPort = getPortOrDefaultFromArgs(args, 3, 9090);
+
+    // Registers all RPC views.
+    RpcViews.registerAllViews();
+
+    // Registers logging trace exporter.
+    LoggingTraceExporter.register();
+
+    // Starts a HTTP server and registers all Zpages to it.
+    ZPageHandlers.startHttpServerAndRegisterAll(zPagePort);
+    logger.info("ZPages server starts at localhost:" + zPagePort);
+
+    // Registers Stackdriver exporters.
+    if (cloudProjectId != null) {
+      StackdriverTraceExporter.createAndRegister(
+          StackdriverTraceConfiguration.builder().setProjectId(cloudProjectId).build());
+      StackdriverStatsExporter.createAndRegister(
+          StackdriverStatsConfiguration.builder()
+              .setProjectId(cloudProjectId)
+              .setExportInterval(Duration.create(15, 0))
+              .build());
+    }
+
+    // Register Prometheus exporters and export metrics to a Prometheus HTTPServer.
+    PrometheusStatsCollector.createAndRegister();
+    HTTPServer prometheusServer = new HTTPServer(prometheusPort, true);
+
+    // Start the RPC server. You shouldn't see any output from gRPC before this.
+    logger.info("gRPC starting.");
+    final HelloWorldServer server = new HelloWorldServer(serverPort);
+    server.start();
+    server.blockUntilShutdown();
+  }
+
+  static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
+
+    @Override
+    public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
+      Span span = tracer.getCurrentSpan();
+      span.putAttribute("my_attribute", AttributeValue.stringAttributeValue("red"));
+      span.addAnnotation(
+          "Constructing greeting.",
+          ImmutableMap.of(
+              "name", AttributeValue.stringAttributeValue(req.getName()),
+              "name length", AttributeValue.longAttributeValue(req.getName().length())));
+      sleepFor(10);
+      performWork(span);
+      span.addAnnotation("Sleeping.");
+      sleepFor(30);
+      HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
+      responseObserver.onNext(reply);
+      responseObserver.onCompleted();
+      logger.info("SayHello RPC handled.");
+    }
+  }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldUtils.java b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldUtils.java
new file mode 100644
index 0000000..55d6c22
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldUtils.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018, 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.examples.grpc.helloworld;
+
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/** Util methods. */
+final class HelloWorldUtils {
+
+  private static final Logger logger = Logger.getLogger(HelloWorldUtils.class.getName());
+
+  static int getPortOrDefaultFromArgs(String[] args, int index, int defaultPort) {
+    int portNumber = defaultPort;
+    if (index < args.length) {
+      try {
+        portNumber = Integer.parseInt(args[index]);
+      } catch (NumberFormatException e) {
+        logger.warning(
+            String.format("Port %s is invalid, use default port %d.", args[index], defaultPort));
+      }
+    }
+    return portNumber;
+  }
+
+  static String getStringOrDefaultFromArgs(
+      String[] args, int index, @Nullable String defaultString) {
+    String s = defaultString;
+    if (index < args.length) {
+      s = args[index];
+    }
+    return s;
+  }
+
+  private HelloWorldUtils() {}
+}
diff --git a/examples/src/main/java/io/opencensus/examples/helloworld/QuickStart.java b/examples/src/main/java/io/opencensus/examples/helloworld/QuickStart.java
new file mode 100644
index 0000000..c71e0f3
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/helloworld/QuickStart.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018, 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.examples.helloworld;
+
+import io.opencensus.common.Scope;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.Tags;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Random;
+import java.util.logging.Logger;
+
+/** Simple program that collects data for video size. */
+public final class QuickStart {
+
+  private static final Logger logger = Logger.getLogger(QuickStart.class.getName());
+
+  private static final Tagger tagger = Tags.getTagger();
+  private static final ViewManager viewManager = Stats.getViewManager();
+  private static final StatsRecorder statsRecorder = Stats.getStatsRecorder();
+  private static final Tracer tracer = Tracing.getTracer();
+
+  // frontendKey allows us to break down the recorded data.
+  private static final TagKey FRONTEND_KEY = TagKey.create("my.org/keys/frontend");
+
+  // videoSize will measure the size of processed videos.
+  private static final MeasureLong VIDEO_SIZE =
+      MeasureLong.create("my.org/measure/video_size", "size of processed videos", "By");
+
+  private static final long MiB = 1 << 20;
+
+  // Create view to see the processed video size distribution broken down by frontend.
+  // The view has bucket boundaries (0, 16 * MiB, 65536 * MiB) that will group measure
+  // values into histogram buckets.
+  private static final View.Name VIDEO_SIZE_VIEW_NAME = View.Name.create("my.org/views/video_size");
+  private static final View VIDEO_SIZE_VIEW =
+      View.create(
+          VIDEO_SIZE_VIEW_NAME,
+          "processed video size over time",
+          VIDEO_SIZE,
+          Aggregation.Distribution.create(
+              BucketBoundaries.create(Arrays.asList(0.0, 16.0 * MiB, 256.0 * MiB))),
+          Collections.singletonList(FRONTEND_KEY));
+
+  /** Main launcher for the QuickStart example. */
+  public static void main(String[] args) throws InterruptedException {
+    TagContextBuilder tagContextBuilder =
+        tagger.currentBuilder().put(FRONTEND_KEY, TagValue.create("mobile-ios9.3.5"));
+    SpanBuilder spanBuilder =
+        tracer
+            .spanBuilder("my.org/ProcessVideo")
+            .setRecordEvents(true)
+            .setSampler(Samplers.alwaysSample());
+    viewManager.registerView(VIDEO_SIZE_VIEW);
+    LoggingTraceExporter.register();
+
+    // Process video.
+    // Record the processed video size.
+    try (Scope scopedTags = tagContextBuilder.buildScoped();
+        Scope scopedSpan = spanBuilder.startScopedSpan()) {
+      tracer.getCurrentSpan().addAnnotation("Start processing video.");
+      // Sleep for [0,10] milliseconds to fake work.
+      Thread.sleep(new Random().nextInt(10) + 1);
+      statsRecorder.newMeasureMap().put(VIDEO_SIZE, 25 * MiB).record();
+      tracer.getCurrentSpan().addAnnotation("Finished processing video.");
+    } catch (Exception e) {
+      tracer.getCurrentSpan().addAnnotation("Exception thrown when processing video.");
+      tracer.getCurrentSpan().setStatus(Status.UNKNOWN);
+      logger.severe(e.getMessage());
+    }
+
+    logger.info("Wait longer than the reporting duration...");
+    // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+    // TODO(songya): remove the gap once we add a shutdown hook for exporting unflushed spans.
+    Thread.sleep(5100);
+    ViewData viewData = viewManager.getView(VIDEO_SIZE_VIEW_NAME);
+    logger.info(
+        String.format("Recorded stats for %s:\n %s", VIDEO_SIZE_VIEW_NAME.asString(), viewData));
+  }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/tags/TagContextExample.java b/examples/src/main/java/io/opencensus/examples/tags/TagContextExample.java
new file mode 100644
index 0000000..727c5fb
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/tags/TagContextExample.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-17, 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.examples.tags;
+
+import io.opencensus.common.Scope;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.Tags;
+
+/** Simple program that uses {@link TagContext}. */
+public class TagContextExample {
+
+  private static final TagKey K1 = TagKey.create("k1");
+  private static final TagKey K2 = TagKey.create("k2");
+  private static final TagKey K3 = TagKey.create("k3");
+  private static final TagKey K4 = TagKey.create("k4");
+
+  private static final TagValue V1 = TagValue.create("v1");
+  private static final TagValue V2 = TagValue.create("v2");
+  private static final TagValue V3 = TagValue.create("v3");
+  private static final TagValue V4 = TagValue.create("v4");
+
+  private static final String UNIT = "1";
+  private static final MeasureDouble M1 = MeasureDouble.create("m1", "1st test metric", UNIT);
+  private static final MeasureDouble M2 = MeasureDouble.create("m2", "2nd test metric", UNIT);
+
+  private static final Tagger tagger = Tags.getTagger();
+  private static final StatsRecorder statsRecorder = Stats.getStatsRecorder();
+
+  private TagContextExample() {}
+
+  /**
+   * Main method.
+   *
+   * @param args the main arguments.
+   */
+  public static void main(String[] args) {
+    System.out.println("Hello Stats World");
+    System.out.println("Default Tags: " + tagger.empty());
+    System.out.println("Current Tags: " + tagger.getCurrentTagContext());
+    TagContext tags1 = tagger.emptyBuilder().put(K1, V1).put(K2, V2).build();
+    try (Scope scopedTagCtx1 = tagger.withTagContext(tags1)) {
+      System.out.println("  Current Tags: " + tagger.getCurrentTagContext());
+      System.out.println(
+          "  Current == Default + tags1: " + tagger.getCurrentTagContext().equals(tags1));
+      TagContext tags2 = tagger.toBuilder(tags1).put(K3, V3).put(K4, V4).build();
+      try (Scope scopedTagCtx2 = tagger.withTagContext(tags2)) {
+        System.out.println("    Current Tags: " + tagger.getCurrentTagContext());
+        System.out.println(
+            "    Current == Default + tags1 + tags2: "
+                + tagger.getCurrentTagContext().equals(tags2));
+        statsRecorder.newMeasureMap().put(M1, 0.2).put(M2, 0.4).record();
+      }
+    }
+    System.out.println(
+        "Current == Default: " + tagger.getCurrentTagContext().equals(tagger.empty()));
+  }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/trace/MultiSpansContextTracing.java b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansContextTracing.java
new file mode 100644
index 0000000..c8df144
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansContextTracing.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.examples.trace;
+
+import static io.opencensus.examples.trace.Utils.sleep;
+
+import io.opencensus.common.Scope;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.samplers.Samplers;
+
+/**
+ * Example showing how to create a child {@link Span}, install it to the current context and add
+ * annotations.
+ */
+public final class MultiSpansContextTracing {
+  // Per class Tracer.
+  private static final Tracer tracer = Tracing.getTracer();
+
+  private MultiSpansContextTracing() {}
+
+  private static void doSomeOtherWork() {
+    tracer.getCurrentSpan().addAnnotation("Annotation to the child Span");
+  }
+
+  private static void doSomeMoreWork() {
+    // Create a child Span of the current Span.
+    Span span = tracer.spanBuilder("MyChildSpan").startSpan();
+    try (Scope ws = tracer.withSpan(span)) {
+      doSomeOtherWork();
+    }
+    span.end();
+  }
+
+  private static void doWork() {
+    tracer.getCurrentSpan().addAnnotation("Annotation to the root Span before child is created.");
+    doSomeMoreWork();
+    tracer.getCurrentSpan().addAnnotation("Annotation to the root Span after child is ended.");
+  }
+
+  /**
+   * Main method.
+   *
+   * @param args the main arguments.
+   */
+  public static void main(String[] args) {
+
+    // WARNING: Be careful before you set sampler value to always sample, especially in
+    // production environment. Trace data is often very large in size and is expensive to
+    // collect. This is why rather than collecting traces for every request(i.e. alwaysSample),
+    // downsampling is prefered.
+    //
+    // By default, OpenCensus provides a probabilistic sampler that will trace once in every
+    // 10,000 requests, that's why if default probabilistic sampler is used
+    // you might not see trace data printed or exported and this is expected behavior.
+
+    TraceConfig traceConfig = Tracing.getTraceConfig();
+    traceConfig.updateActiveTraceParams(
+        traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
+
+    LoggingTraceExporter.register();
+    Span span = tracer.spanBuilderWithExplicitParent("MyRootSpan", null).startSpan();
+    try (Scope ws = tracer.withSpan(span)) {
+      doWork();
+    }
+    span.end();
+
+    // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+    // Spans are exported every 5 seconds
+    sleep(5100);
+  }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/trace/MultiSpansScopedTracing.java b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansScopedTracing.java
new file mode 100644
index 0000000..5cfc9df
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansScopedTracing.java
@@ -0,0 +1,85 @@
+/*
+ * 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.examples.trace;
+
+import static io.opencensus.examples.trace.Utils.sleep;
+
+import io.opencensus.common.Scope;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.samplers.Samplers;
+
+/**
+ * Example showing how to create a child {@link Span} using scoped Spans, install it in the current
+ * context, and add annotations.
+ */
+public final class MultiSpansScopedTracing {
+  // Per class Tracer.
+  private static final Tracer tracer = Tracing.getTracer();
+
+  private MultiSpansScopedTracing() {}
+
+  private static void doSomeOtherWork() {
+    tracer.getCurrentSpan().addAnnotation("Annotation to the child Span");
+  }
+
+  private static void doSomeMoreWork() {
+    // Create a child Span of the current Span.
+    try (Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan()) {
+      doSomeOtherWork();
+    }
+  }
+
+  private static void doWork() {
+    tracer.getCurrentSpan().addAnnotation("Annotation to the root Span before child is created.");
+    doSomeMoreWork();
+    tracer.getCurrentSpan().addAnnotation("Annotation to the root Span after child is ended.");
+  }
+
+  /**
+   * Main method.
+   *
+   * @param args the main arguments.
+   */
+  public static void main(String[] args) {
+
+    // WARNING: Be careful before you set sampler value to always sample, especially in
+    // production environment. Trace data is often very large in size and is expensive to
+    // collect. This is why rather than collecting traces for every request(i.e. alwaysSample),
+    // downsampling is prefered.
+    //
+    // By default, OpenCensus provides a probabilistic sampler that will trace once in every
+    // 10,000 requests, that's why if default probabilistic sampler is used
+    // you might not see trace data printed or exported and this is expected behavior.
+
+    TraceConfig traceConfig = Tracing.getTraceConfig();
+    traceConfig.updateActiveTraceParams(
+        traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
+
+    LoggingTraceExporter.register();
+    try (Scope ss = tracer.spanBuilderWithExplicitParent("MyRootSpan", null).startScopedSpan()) {
+      doWork();
+    }
+
+    // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+    // Spans are exported every 5 seconds
+    sleep(5100);
+  }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/trace/MultiSpansTracing.java b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansTracing.java
new file mode 100644
index 0000000..fae4e3f
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansTracing.java
@@ -0,0 +1,72 @@
+/*
+ * 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.examples.trace;
+
+import static io.opencensus.examples.trace.Utils.sleep;
+
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.samplers.Samplers;
+
+/** Example showing how to directly create a child {@link Span} and add annotations. */
+public final class MultiSpansTracing {
+  // Per class Tracer.
+  private static final Tracer tracer = Tracing.getTracer();
+
+  private MultiSpansTracing() {}
+
+  private static void doWork() {
+    Span rootSpan = tracer.spanBuilderWithExplicitParent("MyRootSpan", null).startSpan();
+    rootSpan.addAnnotation("Annotation to the root Span before child is created.");
+    Span childSpan = tracer.spanBuilderWithExplicitParent("MyChildSpan", rootSpan).startSpan();
+    childSpan.addAnnotation("Annotation to the child Span");
+    childSpan.end();
+    rootSpan.addAnnotation("Annotation to the root Span after child is ended.");
+    rootSpan.end();
+  }
+
+  /**
+   * Main method.
+   *
+   * @param args the main arguments.
+   */
+  public static void main(String[] args) {
+
+    // WARNING: Be careful before you set sampler value to always sample, especially in
+    // production environment. Trace data is often very large in size and is expensive to
+    // collect. This is why rather than collecting traces for every request(i.e. alwaysSample),
+    // downsampling is prefered.
+    //
+    // By default, OpenCensus provides a probabilistic sampler that will trace once in every
+    // 10,000 requests, that's why if default probabilistic sampler is used
+    // you might not see trace data printed or exported and this is expected behavior.
+
+    TraceConfig traceConfig = Tracing.getTraceConfig();
+    traceConfig.updateActiveTraceParams(
+        traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
+
+    LoggingTraceExporter.register();
+    doWork();
+
+    // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+    // Spans are exported every 5 seconds
+    sleep(5100);
+  }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/trace/Utils.java b/examples/src/main/java/io/opencensus/examples/trace/Utils.java
new file mode 100644
index 0000000..9f0338a
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/trace/Utils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018, 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.examples.trace;
+
+import java.util.logging.Logger;
+
+/** Util methods. */
+final class Utils {
+
+  private static final Logger logger = Logger.getLogger(Utils.class.getName());
+
+  static void sleep(int ms) {
+    // A helper to avoid try-catch when invoking Thread.sleep so that
+    // sleeps can be succinct and not permeated by exception handling.
+    try {
+      Thread.sleep(ms);
+    } catch (Exception e) {
+      logger.warning((String.format("Failed to sleep for %dms. Exception: %s", ms, e)));
+    }
+  }
+
+  private Utils() {}
+}
diff --git a/examples/src/main/java/io/opencensus/examples/zpages/ZPagesTester.java b/examples/src/main/java/io/opencensus/examples/zpages/ZPagesTester.java
new file mode 100644
index 0000000..282b40e
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/zpages/ZPagesTester.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.examples.zpages;
+
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.grpc.metrics.RpcMeasureConstants;
+import io.opencensus.contrib.grpc.metrics.RpcViews;
+import io.opencensus.contrib.zpages.ZPageHandlers;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.Tags;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collections;
+
+/** Testing only class for the UI. */
+public class ZPagesTester {
+
+  private ZPagesTester() {}
+
+  private static final Tagger tagger = Tags.getTagger();
+  private static final Tracer tracer = Tracing.getTracer();
+  private static final StatsRecorder statsRecorder = Stats.getStatsRecorder();
+
+  private static final String SPAN_NAME = "ExampleSpan";
+  private static final TagValue METHOD = TagValue.create("ExampleMethod");
+
+  private static void recordExampleData() throws InterruptedException {
+    Tracing.getExportComponent()
+        .getSampledSpanStore()
+        .registerSpanNamesForCollection(Collections.singletonList(SPAN_NAME));
+    RpcViews.registerAllViews(); // Use old RPC constants to get interval stats.
+    SpanBuilder spanBuilder =
+        tracer.spanBuilder(SPAN_NAME).setRecordEvents(true).setSampler(Samplers.alwaysSample());
+
+    try (Scope scope = spanBuilder.startScopedSpan()) {
+      tracer.getCurrentSpan().addAnnotation("Starts recording.");
+      MeasureMap measureMap =
+          statsRecorder
+              .newMeasureMap()
+              // Client measurements.
+              .put(RpcMeasureConstants.RPC_CLIENT_STARTED_COUNT, 1)
+              .put(RpcMeasureConstants.RPC_CLIENT_FINISHED_COUNT, 1)
+              .put(RpcMeasureConstants.RPC_CLIENT_ROUNDTRIP_LATENCY, 1.0)
+              .put(RpcMeasureConstants.RPC_CLIENT_REQUEST_COUNT, 1)
+              .put(RpcMeasureConstants.RPC_CLIENT_RESPONSE_COUNT, 1)
+              .put(RpcMeasureConstants.RPC_CLIENT_REQUEST_BYTES, 1e5)
+              .put(RpcMeasureConstants.RPC_CLIENT_RESPONSE_BYTES, 1e5)
+              .put(RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES, 1e5)
+              .put(RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES, 1e5)
+              // Server measurements.
+              .put(RpcMeasureConstants.RPC_SERVER_STARTED_COUNT, 1)
+              .put(RpcMeasureConstants.RPC_SERVER_FINISHED_COUNT, 1)
+              .put(RpcMeasureConstants.RPC_SERVER_SERVER_LATENCY, 1.0)
+              .put(RpcMeasureConstants.RPC_SERVER_REQUEST_COUNT, 1)
+              .put(RpcMeasureConstants.RPC_SERVER_RESPONSE_COUNT, 1)
+              .put(RpcMeasureConstants.RPC_SERVER_REQUEST_BYTES, 1e5)
+              .put(RpcMeasureConstants.RPC_SERVER_RESPONSE_BYTES, 1e5)
+              .put(RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES, 1e5)
+              .put(RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES, 1e5);
+      measureMap.record(
+          tagger
+              .currentBuilder()
+              .put(RpcMeasureConstants.RPC_STATUS, TagValue.create("OK"))
+              .put(RpcMeasureConstants.RPC_METHOD, METHOD)
+              .build());
+      MeasureMap measureMapErrors =
+          statsRecorder
+              .newMeasureMap()
+              .put(RpcMeasureConstants.RPC_CLIENT_ERROR_COUNT, 1)
+              .put(RpcMeasureConstants.RPC_SERVER_ERROR_COUNT, 1);
+      measureMapErrors.record(
+          tagger
+              .currentBuilder()
+              .put(RpcMeasureConstants.RPC_STATUS, TagValue.create("UNKNOWN"))
+              .put(RpcMeasureConstants.RPC_METHOD, METHOD)
+              .build());
+
+      Thread.sleep(200); // sleep for fake work.
+      tracer.getCurrentSpan().addAnnotation("Finish recording.");
+    }
+  }
+
+  /** Main method. */
+  public static void main(String[] args) throws Exception {
+    ZPageHandlers.startHttpServerAndRegisterAll(8080);
+    recordExampleData();
+  }
+}
diff --git a/examples/src/main/proto/helloworld.proto b/examples/src/main/proto/helloworld.proto
new file mode 100644
index 0000000..1bd7930
--- /dev/null
+++ b/examples/src/main/proto/helloworld.proto
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "io.opencensus.examples.grpc.helloworld";
+option java_outer_classname = "HelloWorldProto";
+
+package helloworld;
+
+// The greeting service definition.
+service Greeter {
+  // Sends a greeting
+  rpc SayHello (HelloRequest) returns (HelloReply) {}
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+  string name = 1;
+}
+
+// The response message containing the greetings
+message HelloReply {
+  string message = 1;
+}
diff --git a/exporters/stats/prometheus/README.md b/exporters/stats/prometheus/README.md
new file mode 100644
index 0000000..fa19efc
--- /dev/null
+++ b/exporters/stats/prometheus/README.md
@@ -0,0 +1,81 @@
+# OpenCensus Prometheus Stats Exporter
+
+The *OpenCensus Prometheus Stats Exporter* is a stats exporter that exports data to 
+Prometheus. [Prometheus](https://prometheus.io/) is an open-source systems monitoring and alerting 
+toolkit originally built at [SoundCloud](https://soundcloud.com/).
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you need to install, configure and start Prometheus first. Follow the 
+instructions [here](https://prometheus.io/docs/introduction/first_steps/).
+
+### Hello "Prometheus Stats"
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-exporter-stats-prometheus</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-stats-prometheus:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+ 
+```java
+public class MyMainClass {
+  public static void main(String[] args) {
+    // Creates a PrometheusStatsCollector and registers it to the default Prometheus registry.
+    PrometheusStatsCollector.createAndRegister();
+    
+    // Uses a simple Prometheus HTTPServer to export metrics. 
+    // You can use a Prometheus PushGateway instead, though that's discouraged by Prometheus:
+    // https://prometheus.io/docs/practices/pushing/#should-i-be-using-the-pushgateway.
+    io.prometheus.client.exporter.HTTPServer server = 
+      new HTTPServer(/*host*/ "localhost", /*port*/  9091, /*daemon*/ true);
+    
+    // Your code here.
+    // ...
+  }
+}
+```
+
+In this example, you should be able to see all the OpenCensus Prometheus metrics by visiting 
+localhost:9091/metrics. Every time when you visit localhost:9091/metrics, the metrics will be 
+collected from OpenCensus library and refreshed.
+
+#### Exporting
+
+After collecting stats from OpenCensus, there are multiple options for exporting them. 
+See [Exporting via HTTP](https://github.com/prometheus/client_java#http), [Exporting to a Pushgateway](https://github.com/prometheus/client_java#exporting-to-a-pushgateway)
+and [Bridges](https://github.com/prometheus/client_java#bridges).
+
+#### Java Versions
+
+Java 7 or above is required for using this exporter.
+
+## FAQ
diff --git a/exporters/stats/prometheus/build.gradle b/exporters/stats/prometheus/build.gradle
new file mode 100644
index 0000000..fe8563c
--- /dev/null
+++ b/exporters/stats/prometheus/build.gradle
@@ -0,0 +1,19 @@
+description = 'OpenCensus Stats Prometheus Exporter'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.7
+    it.targetCompatibility = 1.7
+}
+
+dependencies {
+    compileOnly libraries.auto_value
+
+    compile project(':opencensus-api'),
+            libraries.guava,
+            libraries.prometheus_simpleclient
+
+    testCompile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
\ No newline at end of file
diff --git a/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java
new file mode 100644
index 0000000..288813d
--- /dev/null
+++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import static io.prometheus.client.Collector.doubleToGoString;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.prometheus.client.Collector;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.Collector.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Util methods to convert OpenCensus Stats data models to Prometheus data models.
+ *
+ * <p>Each OpenCensus {@link View} will be converted to a Prometheus {@link MetricFamilySamples}
+ * with no {@link Sample}s, and is used for registering Prometheus {@code Metric}s. Only {@code
+ * Cumulative} views are supported. All views are under namespace "opencensus".
+ *
+ * <p>{@link Aggregation} will be converted to a corresponding Prometheus {@link Type}. {@link Sum}
+ * will be {@link Type#UNTYPED}, {@link Count} will be {@link Type#COUNTER}, {@link
+ * Aggregation.Mean} will be {@link Type#SUMMARY}, {@link Aggregation.LastValue} will be {@link
+ * Type#GAUGE} and {@link Distribution} will be {@link Type#HISTOGRAM}. Please note we cannot set
+ * bucket boundaries for custom {@link Type#HISTOGRAM}.
+ *
+ * <p>Each OpenCensus {@link ViewData} will be converted to a Prometheus {@link
+ * MetricFamilySamples}, and each {@code Row} of the {@link ViewData} will be converted to
+ * Prometheus {@link Sample}s.
+ *
+ * <p>{@link SumDataDouble}, {@link SumDataLong}, {@link LastValueDataDouble}, {@link
+ * LastValueDataLong} and {@link CountData} will be converted to a single {@link Sample}. {@link
+ * AggregationData.MeanData} will be converted to two {@link Sample}s sum and count. {@link
+ * DistributionData} will be converted to a list of {@link Sample}s that have the sum, count and
+ * histogram buckets.
+ *
+ * <p>{@link TagKey} and {@link TagValue} will be converted to Prometheus {@code LabelName} and
+ * {@code LabelValue}. {@code Null} {@link TagValue} will be converted to an empty string.
+ *
+ * <p>Please note that Prometheus Metric and Label name can only have alphanumeric characters and
+ * underscore. All other characters will be sanitized by underscores.
+ */
+@SuppressWarnings("deprecation")
+final class PrometheusExportUtils {
+
+  @VisibleForTesting static final String SAMPLE_SUFFIX_BUCKET = "_bucket";
+  @VisibleForTesting static final String SAMPLE_SUFFIX_COUNT = "_count";
+  @VisibleForTesting static final String SAMPLE_SUFFIX_SUM = "_sum";
+  @VisibleForTesting static final String LABEL_NAME_BUCKET_BOUND = "le";
+
+  private static final Function<Object, Type> TYPE_UNTYPED_FUNCTION =
+      Functions.returnConstant(Type.UNTYPED);
+  private static final Function<Object, Type> TYPE_COUNTER_FUNCTION =
+      Functions.returnConstant(Type.COUNTER);
+  private static final Function<Object, Type> TYPE_HISTOGRAM_FUNCTION =
+      Functions.returnConstant(Type.HISTOGRAM);
+  private static final Function<Object, Type> TYPE_GAUGE_FUNCTION =
+      Functions.returnConstant(Type.GAUGE);
+
+  // Converts a ViewData to a Prometheus MetricFamilySamples.
+  static MetricFamilySamples createMetricFamilySamples(ViewData viewData) {
+    View view = viewData.getView();
+    String name = Collector.sanitizeMetricName(view.getName().asString());
+    Type type = getType(view.getAggregation(), view.getWindow());
+    List<String> labelNames = convertToLabelNames(view.getColumns());
+    List<Sample> samples = Lists.newArrayList();
+    for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+        viewData.getAggregationMap().entrySet()) {
+      samples.addAll(
+          getSamples(name, labelNames, entry.getKey(), entry.getValue(), view.getAggregation()));
+    }
+    return new MetricFamilySamples(name, type, view.getDescription(), samples);
+  }
+
+  // Converts a View to a Prometheus MetricFamilySamples.
+  // Used only for Prometheus metric registry, should not contain any actual samples.
+  static MetricFamilySamples createDescribableMetricFamilySamples(View view) {
+    String name = Collector.sanitizeMetricName(view.getName().asString());
+    Type type = getType(view.getAggregation(), view.getWindow());
+    List<String> labelNames = convertToLabelNames(view.getColumns());
+    if (containsDisallowedLeLabelForHistogram(labelNames, type)) {
+      throw new IllegalStateException(
+          "Prometheus Histogram cannot have a label named 'le', "
+              + "because it is a reserved label for bucket boundaries. "
+              + "Please remove this tag key from your view.");
+    }
+    return new MetricFamilySamples(
+        name, type, view.getDescription(), Collections.<Sample>emptyList());
+  }
+
+  @VisibleForTesting
+  static Type getType(Aggregation aggregation, View.AggregationWindow window) {
+    if (!(window instanceof View.AggregationWindow.Cumulative)) {
+      return Type.UNTYPED;
+    }
+    return aggregation.match(
+        TYPE_UNTYPED_FUNCTION, // SUM
+        TYPE_COUNTER_FUNCTION, // COUNT
+        TYPE_HISTOGRAM_FUNCTION, // DISTRIBUTION
+        TYPE_GAUGE_FUNCTION, // LAST VALUE
+        new Function<Aggregation, Type>() {
+          @Override
+          public Type apply(Aggregation arg) {
+            if (arg instanceof Aggregation.Mean) {
+              return Type.SUMMARY;
+            }
+            return Type.UNTYPED;
+          }
+        });
+  }
+
+  // Converts a row in ViewData (a.k.a Entry<List<TagValue>, AggregationData>) to a list of
+  // Prometheus Samples.
+  @VisibleForTesting
+  static List<Sample> getSamples(
+      final String name,
+      final List<String> labelNames,
+      List</*@Nullable*/ TagValue> tagValues,
+      AggregationData aggregationData,
+      final Aggregation aggregation) {
+    Preconditions.checkArgument(
+        labelNames.size() == tagValues.size(), "Label names and tag values have different sizes.");
+    final List<Sample> samples = Lists.newArrayList();
+    final List<String> labelValues = new ArrayList<String>(tagValues.size());
+    for (TagValue tagValue : tagValues) {
+      String labelValue = tagValue == null ? "" : tagValue.asString();
+      labelValues.add(labelValue);
+    }
+
+    aggregationData.match(
+        new Function<SumDataDouble, Void>() {
+          @Override
+          public Void apply(SumDataDouble arg) {
+            samples.add(new Sample(name, labelNames, labelValues, arg.getSum()));
+            return null;
+          }
+        },
+        new Function<SumDataLong, Void>() {
+          @Override
+          public Void apply(SumDataLong arg) {
+            samples.add(new Sample(name, labelNames, labelValues, arg.getSum()));
+            return null;
+          }
+        },
+        new Function<CountData, Void>() {
+          @Override
+          public Void apply(CountData arg) {
+            samples.add(new Sample(name, labelNames, labelValues, arg.getCount()));
+            return null;
+          }
+        },
+        new Function<DistributionData, Void>() {
+          @Override
+          public Void apply(DistributionData arg) {
+            // For histogram buckets, manually add the bucket boundaries as "le" labels. See
+            // https://github.com/prometheus/client_java/commit/ed184d8e50c82e98bb2706723fff764424840c3a#diff-c505abbde72dd6bf36e89917b3469404R241
+            @SuppressWarnings("unchecked")
+            Distribution distribution = (Distribution) aggregation;
+            List<Double> boundaries = distribution.getBucketBoundaries().getBoundaries();
+            List<String> labelNamesWithLe = new ArrayList<String>(labelNames);
+            labelNamesWithLe.add(LABEL_NAME_BUCKET_BOUND);
+            long cumulativeCount = 0;
+            for (int i = 0; i < arg.getBucketCounts().size(); i++) {
+              List<String> labelValuesWithLe = new ArrayList<String>(labelValues);
+              // The label value of "le" is the upper inclusive bound.
+              // For the last bucket, it should be "+Inf".
+              String bucketBoundary =
+                  doubleToGoString(
+                      i < boundaries.size() ? boundaries.get(i) : Double.POSITIVE_INFINITY);
+              labelValuesWithLe.add(bucketBoundary);
+              cumulativeCount += arg.getBucketCounts().get(i);
+              samples.add(
+                  new MetricFamilySamples.Sample(
+                      name + SAMPLE_SUFFIX_BUCKET,
+                      labelNamesWithLe,
+                      labelValuesWithLe,
+                      cumulativeCount));
+            }
+
+            samples.add(
+                new MetricFamilySamples.Sample(
+                    name + SAMPLE_SUFFIX_COUNT, labelNames, labelValues, arg.getCount()));
+            samples.add(
+                new MetricFamilySamples.Sample(
+                    name + SAMPLE_SUFFIX_SUM,
+                    labelNames,
+                    labelValues,
+                    arg.getCount() * arg.getMean()));
+            return null;
+          }
+        },
+        new Function<LastValueDataDouble, Void>() {
+          @Override
+          public Void apply(LastValueDataDouble arg) {
+            samples.add(new Sample(name, labelNames, labelValues, arg.getLastValue()));
+            return null;
+          }
+        },
+        new Function<LastValueDataLong, Void>() {
+          @Override
+          public Void apply(LastValueDataLong arg) {
+            samples.add(new Sample(name, labelNames, labelValues, arg.getLastValue()));
+            return null;
+          }
+        },
+        new Function<AggregationData, Void>() {
+          @Override
+          public Void apply(AggregationData arg) {
+            // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+            // we need to continue supporting Mean, since it could still be used by users and some
+            // deprecated RPC views.
+            if (arg instanceof AggregationData.MeanData) {
+              AggregationData.MeanData meanData = (AggregationData.MeanData) arg;
+              samples.add(
+                  new MetricFamilySamples.Sample(
+                      name + SAMPLE_SUFFIX_COUNT, labelNames, labelValues, meanData.getCount()));
+              samples.add(
+                  new MetricFamilySamples.Sample(
+                      name + SAMPLE_SUFFIX_SUM,
+                      labelNames,
+                      labelValues,
+                      meanData.getCount() * meanData.getMean()));
+              return null;
+            }
+            throw new IllegalArgumentException("Unknown Aggregation.");
+          }
+        });
+
+    return samples;
+  }
+
+  // Converts the list of tag keys to a list of string label names. Also sanitizes the tag keys.
+  @VisibleForTesting
+  static List<String> convertToLabelNames(List<TagKey> tagKeys) {
+    final List<String> labelNames = new ArrayList<String>(tagKeys.size());
+    for (TagKey tagKey : tagKeys) {
+      labelNames.add(Collector.sanitizeMetricName(tagKey.getName()));
+    }
+    return labelNames;
+  }
+
+  // Returns true if there is an "le" label name in histogram label names, returns false otherwise.
+  // Similar check to
+  // https://github.com/prometheus/client_java/commit/ed184d8e50c82e98bb2706723fff764424840c3a#diff-c505abbde72dd6bf36e89917b3469404R78
+  static boolean containsDisallowedLeLabelForHistogram(List<String> labelNames, Type type) {
+    if (!Type.HISTOGRAM.equals(type)) {
+      return false;
+    }
+    for (String label : labelNames) {
+      if (LABEL_NAME_BUCKET_BOUND.equals(label)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private PrometheusExportUtils() {}
+}
diff --git a/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java
new file mode 100644
index 0000000..d555c92
--- /dev/null
+++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.containsDisallowedLeLabelForHistogram;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.convertToLabelNames;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.getType;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import io.opencensus.common.Scope;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import io.prometheus.client.Collector;
+import io.prometheus.client.CollectorRegistry;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * OpenCensus Stats {@link Collector} for Prometheus.
+ *
+ * @since 0.12
+ */
+@SuppressWarnings("deprecation")
+public final class PrometheusStatsCollector extends Collector implements Collector.Describable {
+
+  private static final Logger logger = Logger.getLogger(PrometheusStatsCollector.class.getName());
+  private static final Tracer tracer = Tracing.getTracer();
+  private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+
+  private final ViewManager viewManager;
+
+  /**
+   * Creates a {@link PrometheusStatsCollector} and registers it to Prometheus {@link
+   * CollectorRegistry#defaultRegistry}.
+   *
+   * <p>This is equivalent with:
+   *
+   * <pre>{@code
+   * PrometheusStatsCollector.createAndRegister(PrometheusStatsConfiguration.builder().build());
+   * }</pre>
+   *
+   * @throws IllegalArgumentException if a {@code PrometheusStatsCollector} has already been created
+   *     and registered.
+   * @since 0.12
+   */
+  public static void createAndRegister() {
+    new PrometheusStatsCollector(Stats.getViewManager()).register();
+  }
+
+  /**
+   * Creates a {@link PrometheusStatsCollector} and registers it to the given Prometheus {@link
+   * CollectorRegistry} in the {@link PrometheusStatsConfiguration}.
+   *
+   * <p>If {@code CollectorRegistry} of the configuration is not set, the collector will use {@link
+   * CollectorRegistry#defaultRegistry}.
+   *
+   * @throws IllegalArgumentException if a {@code PrometheusStatsCollector} has already been created
+   *     and registered.
+   * @since 0.13
+   */
+  public static void createAndRegister(PrometheusStatsConfiguration configuration) {
+    CollectorRegistry registry = configuration.getRegistry();
+    if (registry == null) {
+      registry = CollectorRegistry.defaultRegistry;
+    }
+    new PrometheusStatsCollector(Stats.getViewManager()).register(registry);
+  }
+
+  @Override
+  public List<MetricFamilySamples> collect() {
+    List<MetricFamilySamples> samples = Lists.newArrayList();
+    Span span =
+        tracer
+            .spanBuilder("ExportStatsToPrometheus")
+            .setSampler(probabilitySampler)
+            .setRecordEvents(true)
+            .startSpan();
+    span.addAnnotation("Collect Prometheus Metric Samples.");
+    Scope scope = tracer.withSpan(span);
+    try {
+      for (View view : viewManager.getAllExportedViews()) {
+        if (containsDisallowedLeLabelForHistogram(
+            convertToLabelNames(view.getColumns()),
+            getType(view.getAggregation(), view.getWindow()))) {
+          continue; // silently skip Distribution views with "le" tag key
+        }
+        try {
+          ViewData viewData = viewManager.getView(view.getName());
+          if (viewData == null) {
+            continue;
+          } else {
+            samples.add(PrometheusExportUtils.createMetricFamilySamples(viewData));
+          }
+        } catch (Throwable e) {
+          logger.log(Level.WARNING, "Exception thrown when collecting metric samples.", e);
+          span.setStatus(
+              Status.UNKNOWN.withDescription(
+                  "Exception thrown when collecting Prometheus Metric Samples: "
+                      + exceptionMessage(e)));
+        }
+      }
+      span.addAnnotation("Finish collecting Prometheus Metric Samples.");
+    } finally {
+      scope.close();
+      span.end();
+    }
+    return samples;
+  }
+
+  @Override
+  public List<MetricFamilySamples> describe() {
+    List<MetricFamilySamples> samples = Lists.newArrayList();
+    Span span =
+        tracer
+            .spanBuilder("DescribeMetricsForPrometheus")
+            .setSampler(probabilitySampler)
+            .setRecordEvents(true)
+            .startSpan();
+    span.addAnnotation("Describe Prometheus Metrics.");
+    Scope scope = tracer.withSpan(span);
+    try {
+      for (View view : viewManager.getAllExportedViews()) {
+        try {
+          samples.add(PrometheusExportUtils.createDescribableMetricFamilySamples(view));
+        } catch (Throwable e) {
+          logger.log(Level.WARNING, "Exception thrown when describing metrics.", e);
+          span.setStatus(
+              Status.UNKNOWN.withDescription(
+                  "Exception thrown when describing Prometheus Metrics: " + exceptionMessage(e)));
+        }
+      }
+      span.addAnnotation("Finish describing Prometheus Metrics.");
+    } finally {
+      scope.close();
+      span.end();
+    }
+    return samples;
+  }
+
+  @VisibleForTesting
+  PrometheusStatsCollector(ViewManager viewManager) {
+    this.viewManager = viewManager;
+    Tracing.getExportComponent()
+        .getSampledSpanStore()
+        .registerSpanNamesForCollection(
+            ImmutableList.of("DescribeMetricsForPrometheus", "ExportStatsToPrometheus"));
+  }
+
+  private static String exceptionMessage(Throwable e) {
+    return e.getMessage() != null ? e.getMessage() : e.getClass().getName();
+  }
+}
diff --git a/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java
new file mode 100644
index 0000000..3e8b95e
--- /dev/null
+++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import com.google.auto.value.AutoValue;
+import io.prometheus.client.CollectorRegistry;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link PrometheusStatsCollector}.
+ *
+ * @since 0.13
+ */
+@AutoValue
+@Immutable
+public abstract class PrometheusStatsConfiguration {
+
+  PrometheusStatsConfiguration() {}
+
+  /**
+   * Returns the Prometheus {@link CollectorRegistry}.
+   *
+   * @return the Prometheus {@code CollectorRegistry}.
+   * @since 0.13
+   */
+  @Nullable
+  public abstract CollectorRegistry getRegistry();
+
+  /**
+   * Returns a new {@link Builder}.
+   *
+   * @return a {@code Builder}.
+   * @since 0.13
+   */
+  public static Builder builder() {
+    return new AutoValue_PrometheusStatsConfiguration.Builder();
+  }
+
+  /**
+   * Builder for {@link PrometheusStatsConfiguration}.
+   *
+   * @since 0.13
+   */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    Builder() {}
+
+    /**
+     * Sets the given Prometheus {@link CollectorRegistry}.
+     *
+     * @param registry the Prometheus {@code CollectorRegistry}.
+     * @return this.
+     * @since 0.13
+     */
+    public abstract Builder setRegistry(CollectorRegistry registry);
+
+    /**
+     * Builds a new {@link PrometheusStatsConfiguration} with current settings.
+     *
+     * @return a {@code PrometheusStatsConfiguration}.
+     * @since 0.13
+     */
+    public abstract PrometheusStatsConfiguration build();
+  }
+}
diff --git a/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java
new file mode 100644
index 0000000..ca8315b
--- /dev/null
+++ b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.LABEL_NAME_BUCKET_BOUND;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.SAMPLE_SUFFIX_BUCKET;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.SAMPLE_SUFFIX_COUNT;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.SAMPLE_SUFFIX_SUM;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.convertToLabelNames;
+
+import com.google.common.collect.ImmutableMap;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.Collector.Type;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PrometheusExportUtils}. */
+@RunWith(JUnit4.class)
+public class PrometheusExportUtilsTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private static final Duration ONE_SECOND = Duration.create(1, 0);
+  private static final Cumulative CUMULATIVE = Cumulative.create();
+  private static final Interval INTERVAL = Interval.create(ONE_SECOND);
+  private static final Sum SUM = Sum.create();
+  private static final Count COUNT = Count.create();
+  private static final Mean MEAN = Mean.create();
+  private static final BucketBoundaries BUCKET_BOUNDARIES =
+      BucketBoundaries.create(Arrays.asList(-5.0, 0.0, 5.0));
+  private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+  private static final LastValue LAST_VALUE = LastValue.create();
+  private static final View.Name VIEW_NAME_1 = View.Name.create("view1");
+  private static final View.Name VIEW_NAME_2 = View.Name.create("view2");
+  private static final View.Name VIEW_NAME_3 = View.Name.create("view-3");
+  private static final View.Name VIEW_NAME_4 = View.Name.create("-view4");
+  private static final String DESCRIPTION = "View description";
+  private static final MeasureDouble MEASURE_DOUBLE =
+      MeasureDouble.create("measure", "description", "1");
+  private static final TagKey K1 = TagKey.create("k1");
+  private static final TagKey K2 = TagKey.create("k2");
+  private static final TagKey K3 = TagKey.create("k-3");
+  private static final TagKey TAG_KEY_LE = TagKey.create(LABEL_NAME_BUCKET_BOUND);
+  private static final TagValue V1 = TagValue.create("v1");
+  private static final TagValue V2 = TagValue.create("v2");
+  private static final TagValue V3 = TagValue.create("v-3");
+  private static final SumDataDouble SUM_DATA_DOUBLE = SumDataDouble.create(-5.5);
+  private static final SumDataLong SUM_DATA_LONG = SumDataLong.create(123456789);
+  private static final CountData COUNT_DATA = CountData.create(12345);
+  private static final MeanData MEAN_DATA = MeanData.create(3.4, 22);
+  private static final DistributionData DISTRIBUTION_DATA =
+      DistributionData.create(4.4, 5, -3.2, 15.7, 135.22, Arrays.asList(0L, 2L, 2L, 1L));
+  private static final LastValueDataDouble LAST_VALUE_DATA_DOUBLE = LastValueDataDouble.create(7.9);
+  private static final LastValueDataLong LAST_VALUE_DATA_LONG = LastValueDataLong.create(66666666);
+  private static final View VIEW1 =
+      View.create(
+          VIEW_NAME_1, DESCRIPTION, MEASURE_DOUBLE, COUNT, Arrays.asList(K1, K2), CUMULATIVE);
+  private static final View VIEW2 =
+      View.create(VIEW_NAME_2, DESCRIPTION, MEASURE_DOUBLE, MEAN, Arrays.asList(K3), CUMULATIVE);
+  private static final View VIEW3 =
+      View.create(
+          VIEW_NAME_3, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(K1), CUMULATIVE);
+  private static final View VIEW4 =
+      View.create(VIEW_NAME_4, DESCRIPTION, MEASURE_DOUBLE, COUNT, Arrays.asList(K1), INTERVAL);
+  private static final View DISTRIBUTION_VIEW_WITH_LE_KEY =
+      View.create(
+          VIEW_NAME_1,
+          DESCRIPTION,
+          MEASURE_DOUBLE,
+          DISTRIBUTION,
+          Arrays.asList(K1, TAG_KEY_LE),
+          CUMULATIVE);
+  private static final CumulativeData CUMULATIVE_DATA =
+      CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+  private static final IntervalData INTERVAL_DATA = IntervalData.create(Timestamp.fromMillis(1000));
+  private static final String SAMPLE_NAME = "view";
+
+  @Test
+  public void testConstants() {
+    assertThat(SAMPLE_SUFFIX_BUCKET).isEqualTo("_bucket");
+    assertThat(SAMPLE_SUFFIX_COUNT).isEqualTo("_count");
+    assertThat(SAMPLE_SUFFIX_SUM).isEqualTo("_sum");
+    assertThat(LABEL_NAME_BUCKET_BOUND).isEqualTo("le");
+  }
+
+  @Test
+  public void getType() {
+    assertThat(PrometheusExportUtils.getType(COUNT, INTERVAL)).isEqualTo(Type.UNTYPED);
+    assertThat(PrometheusExportUtils.getType(COUNT, CUMULATIVE)).isEqualTo(Type.COUNTER);
+    assertThat(PrometheusExportUtils.getType(DISTRIBUTION, CUMULATIVE)).isEqualTo(Type.HISTOGRAM);
+    assertThat(PrometheusExportUtils.getType(SUM, CUMULATIVE)).isEqualTo(Type.UNTYPED);
+    assertThat(PrometheusExportUtils.getType(MEAN, CUMULATIVE)).isEqualTo(Type.SUMMARY);
+    assertThat(PrometheusExportUtils.getType(LAST_VALUE, CUMULATIVE)).isEqualTo(Type.GAUGE);
+  }
+
+  @Test
+  public void createDescribableMetricFamilySamples() {
+    assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW1))
+        .isEqualTo(
+            new MetricFamilySamples(
+                "view1", Type.COUNTER, DESCRIPTION, Collections.<Sample>emptyList()));
+    assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW2))
+        .isEqualTo(
+            new MetricFamilySamples(
+                "view2", Type.SUMMARY, DESCRIPTION, Collections.<Sample>emptyList()));
+    assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW3))
+        .isEqualTo(
+            new MetricFamilySamples(
+                "view_3", Type.HISTOGRAM, DESCRIPTION, Collections.<Sample>emptyList()));
+    assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW4))
+        .isEqualTo(
+            new MetricFamilySamples(
+                "_view4", Type.UNTYPED, DESCRIPTION, Collections.<Sample>emptyList()));
+  }
+
+  @Test
+  public void getSamples() {
+    assertThat(
+            PrometheusExportUtils.getSamples(
+                SAMPLE_NAME,
+                convertToLabelNames(Arrays.asList(K1, K2)),
+                Arrays.asList(V1, V2),
+                SUM_DATA_DOUBLE,
+                SUM))
+        .containsExactly(
+            new Sample(SAMPLE_NAME, Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), -5.5));
+    assertThat(
+            PrometheusExportUtils.getSamples(
+                SAMPLE_NAME,
+                convertToLabelNames(Arrays.asList(K3)),
+                Arrays.asList(V3),
+                SUM_DATA_LONG,
+                SUM))
+        .containsExactly(
+            new Sample(SAMPLE_NAME, Arrays.asList("k_3"), Arrays.asList("v-3"), 123456789));
+    assertThat(
+            PrometheusExportUtils.getSamples(
+                SAMPLE_NAME,
+                convertToLabelNames(Arrays.asList(K1, K3)),
+                Arrays.asList(V1, null),
+                COUNT_DATA,
+                COUNT))
+        .containsExactly(
+            new Sample(SAMPLE_NAME, Arrays.asList("k1", "k_3"), Arrays.asList("v1", ""), 12345));
+    assertThat(
+            PrometheusExportUtils.getSamples(
+                SAMPLE_NAME,
+                convertToLabelNames(Arrays.asList(K3)),
+                Arrays.asList(V3),
+                MEAN_DATA,
+                MEAN))
+        .containsExactly(
+            new Sample(SAMPLE_NAME + "_count", Arrays.asList("k_3"), Arrays.asList("v-3"), 22),
+            new Sample(SAMPLE_NAME + "_sum", Arrays.asList("k_3"), Arrays.asList("v-3"), 74.8))
+        .inOrder();
+    assertThat(
+            PrometheusExportUtils.getSamples(
+                SAMPLE_NAME,
+                convertToLabelNames(Arrays.asList(K1)),
+                Arrays.asList(V1),
+                DISTRIBUTION_DATA,
+                DISTRIBUTION))
+        .containsExactly(
+            new Sample(
+                SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "-5.0"), 0),
+            new Sample(
+                SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "0.0"), 2),
+            new Sample(
+                SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "5.0"), 4),
+            new Sample(
+                SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "+Inf"), 5),
+            new Sample(SAMPLE_NAME + "_count", Arrays.asList("k1"), Arrays.asList("v1"), 5),
+            new Sample(SAMPLE_NAME + "_sum", Arrays.asList("k1"), Arrays.asList("v1"), 22.0))
+        .inOrder();
+    assertThat(
+            PrometheusExportUtils.getSamples(
+                SAMPLE_NAME,
+                convertToLabelNames(Arrays.asList(K1, K2)),
+                Arrays.asList(V1, V2),
+                LAST_VALUE_DATA_DOUBLE,
+                LAST_VALUE))
+        .containsExactly(
+            new Sample(SAMPLE_NAME, Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 7.9));
+    assertThat(
+            PrometheusExportUtils.getSamples(
+                SAMPLE_NAME,
+                convertToLabelNames(Arrays.asList(K3)),
+                Arrays.asList(V3),
+                LAST_VALUE_DATA_LONG,
+                LAST_VALUE))
+        .containsExactly(
+            new Sample(SAMPLE_NAME, Arrays.asList("k_3"), Arrays.asList("v-3"), 66666666));
+  }
+
+  @Test
+  public void getSamples_KeysAndValuesHaveDifferentSizes() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Label names and tag values have different sizes.");
+    PrometheusExportUtils.getSamples(
+        SAMPLE_NAME,
+        convertToLabelNames(Arrays.asList(K1, K2, K3)),
+        Arrays.asList(V1, V2),
+        DISTRIBUTION_DATA,
+        DISTRIBUTION);
+  }
+
+  @Test
+  public void createDescribableMetricFamilySamples_Histogram_DisallowLeLabelName() {
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage(
+        "Prometheus Histogram cannot have a label named 'le', "
+            + "because it is a reserved label for bucket boundaries. "
+            + "Please remove this tag key from your view.");
+    PrometheusExportUtils.createDescribableMetricFamilySamples(DISTRIBUTION_VIEW_WITH_LE_KEY);
+  }
+
+  @Test
+  public void createMetricFamilySamples() {
+    assertThat(
+            PrometheusExportUtils.createMetricFamilySamples(
+                ViewData.create(
+                    VIEW1, ImmutableMap.of(Arrays.asList(V1, V2), COUNT_DATA), CUMULATIVE_DATA)))
+        .isEqualTo(
+            new MetricFamilySamples(
+                "view1",
+                Type.COUNTER,
+                DESCRIPTION,
+                Arrays.asList(
+                    new Sample(
+                        "view1", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 12345))));
+    assertThat(
+            PrometheusExportUtils.createMetricFamilySamples(
+                ViewData.create(
+                    VIEW2, ImmutableMap.of(Arrays.asList(V1), MEAN_DATA), CUMULATIVE_DATA)))
+        .isEqualTo(
+            new MetricFamilySamples(
+                "view2",
+                Type.SUMMARY,
+                DESCRIPTION,
+                Arrays.asList(
+                    new Sample("view2_count", Arrays.asList("k_3"), Arrays.asList("v1"), 22),
+                    new Sample("view2_sum", Arrays.asList("k_3"), Arrays.asList("v1"), 74.8))));
+    assertThat(
+            PrometheusExportUtils.createMetricFamilySamples(
+                ViewData.create(
+                    VIEW3, ImmutableMap.of(Arrays.asList(V3), DISTRIBUTION_DATA), CUMULATIVE_DATA)))
+        .isEqualTo(
+            new MetricFamilySamples(
+                "view_3",
+                Type.HISTOGRAM,
+                DESCRIPTION,
+                Arrays.asList(
+                    new Sample(
+                        "view_3_bucket",
+                        Arrays.asList("k1", "le"),
+                        Arrays.asList("v-3", "-5.0"),
+                        0),
+                    new Sample(
+                        "view_3_bucket", Arrays.asList("k1", "le"), Arrays.asList("v-3", "0.0"), 2),
+                    new Sample(
+                        "view_3_bucket", Arrays.asList("k1", "le"), Arrays.asList("v-3", "5.0"), 4),
+                    new Sample(
+                        "view_3_bucket",
+                        Arrays.asList("k1", "le"),
+                        Arrays.asList("v-3", "+Inf"),
+                        5),
+                    new Sample("view_3_count", Arrays.asList("k1"), Arrays.asList("v-3"), 5),
+                    new Sample("view_3_sum", Arrays.asList("k1"), Arrays.asList("v-3"), 22.0))));
+    assertThat(
+            PrometheusExportUtils.createMetricFamilySamples(
+                ViewData.create(
+                    VIEW4, ImmutableMap.of(Arrays.asList(V1), COUNT_DATA), INTERVAL_DATA)))
+        .isEqualTo(
+            new MetricFamilySamples(
+                "_view4",
+                Type.UNTYPED,
+                DESCRIPTION,
+                Arrays.asList(
+                    new Sample("_view4", Arrays.asList("k1"), Arrays.asList("v1"), 12345))));
+  }
+}
diff --git a/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java
new file mode 100644
index 0000000..3bd9845
--- /dev/null
+++ b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.LABEL_NAME_BUCKET_BOUND;
+import static org.mockito.Mockito.doReturn;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.Collector.Type;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link PrometheusStatsCollector}. */
+@RunWith(JUnit4.class)
+public class PrometheusStatsCollectorTest {
+
+  private static final Cumulative CUMULATIVE = Cumulative.create();
+  private static final BucketBoundaries BUCKET_BOUNDARIES =
+      BucketBoundaries.create(Arrays.asList(-5.0, 0.0, 5.0));
+  private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+  private static final View.Name VIEW_NAME = View.Name.create("view1");
+  private static final String DESCRIPTION = "View description";
+  private static final MeasureDouble MEASURE_DOUBLE =
+      MeasureDouble.create("measure", "description", "1");
+  private static final TagKey K1 = TagKey.create("k1");
+  private static final TagKey K2 = TagKey.create("k2");
+  private static final TagKey LE_TAG_KEY = TagKey.create(LABEL_NAME_BUCKET_BOUND);
+  private static final TagValue V1 = TagValue.create("v1");
+  private static final TagValue V2 = TagValue.create("v2");
+  private static final DistributionData DISTRIBUTION_DATA =
+      DistributionData.create(4.4, 5, -3.2, 15.7, 135.22, Arrays.asList(0L, 2L, 2L, 1L));
+  private static final View VIEW =
+      View.create(
+          VIEW_NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(K1, K2), CUMULATIVE);
+  private static final View VIEW_WITH_LE_TAG_KEY =
+      View.create(
+          VIEW_NAME,
+          DESCRIPTION,
+          MEASURE_DOUBLE,
+          DISTRIBUTION,
+          Arrays.asList(K1, LE_TAG_KEY),
+          CUMULATIVE);
+  private static final CumulativeData CUMULATIVE_DATA =
+      CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+  private static final ViewData VIEW_DATA =
+      ViewData.create(
+          VIEW, ImmutableMap.of(Arrays.asList(V1, V2), DISTRIBUTION_DATA), CUMULATIVE_DATA);
+  private static final ViewData VIEW_DATA_WITH_LE_TAG_KEY =
+      ViewData.create(
+          VIEW_WITH_LE_TAG_KEY,
+          ImmutableMap.of(Arrays.asList(V1, V2), DISTRIBUTION_DATA),
+          CUMULATIVE_DATA);
+
+  @Mock private ViewManager mockViewManager;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    doReturn(ImmutableSet.of(VIEW)).when(mockViewManager).getAllExportedViews();
+    doReturn(VIEW_DATA).when(mockViewManager).getView(VIEW_NAME);
+  }
+
+  @Test
+  public void testCollect() {
+    PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager);
+    String name = "view1";
+    assertThat(collector.collect())
+        .containsExactly(
+            new MetricFamilySamples(
+                "view1",
+                Type.HISTOGRAM,
+                "View description",
+                Arrays.asList(
+                    new Sample(
+                        name + "_bucket",
+                        Arrays.asList("k1", "k2", "le"),
+                        Arrays.asList("v1", "v2", "-5.0"),
+                        0),
+                    new Sample(
+                        name + "_bucket",
+                        Arrays.asList("k1", "k2", "le"),
+                        Arrays.asList("v1", "v2", "0.0"),
+                        2),
+                    new Sample(
+                        name + "_bucket",
+                        Arrays.asList("k1", "k2", "le"),
+                        Arrays.asList("v1", "v2", "5.0"),
+                        4),
+                    new Sample(
+                        name + "_bucket",
+                        Arrays.asList("k1", "k2", "le"),
+                        Arrays.asList("v1", "v2", "+Inf"),
+                        5),
+                    new Sample(
+                        name + "_count", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 5),
+                    new Sample(
+                        name + "_sum",
+                        Arrays.asList("k1", "k2"),
+                        Arrays.asList("v1", "v2"),
+                        22.0))));
+  }
+
+  @Test
+  public void testCollect_SkipDistributionViewWithLeTagKey() {
+    doReturn(ImmutableSet.of(VIEW_WITH_LE_TAG_KEY)).when(mockViewManager).getAllExportedViews();
+    doReturn(VIEW_DATA_WITH_LE_TAG_KEY).when(mockViewManager).getView(VIEW_NAME);
+    PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager);
+    assertThat(collector.collect()).isEmpty();
+  }
+
+  @Test
+  public void testDescribe() {
+    PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager);
+    assertThat(collector.describe())
+        .containsExactly(
+            new MetricFamilySamples(
+                "view1", Type.HISTOGRAM, "View description", Collections.<Sample>emptyList()));
+  }
+
+  @Test
+  public void testCollect_WithNoopViewManager() {
+    PrometheusStatsCollector collector = new PrometheusStatsCollector(Stats.getViewManager());
+    assertThat(collector.collect()).isEmpty();
+  }
+
+  @Test
+  public void testDescribe_WithNoopViewManager() {
+    PrometheusStatsCollector collector = new PrometheusStatsCollector(Stats.getViewManager());
+    assertThat(collector.describe()).isEmpty();
+  }
+}
diff --git a/exporters/stats/signalfx/README.md b/exporters/stats/signalfx/README.md
new file mode 100644
index 0000000..7c61f89
--- /dev/null
+++ b/exporters/stats/signalfx/README.md
@@ -0,0 +1,76 @@
+# OpenCensus SignalFx Stats Exporter
+
+The _OpenCensus SignalFx Stats Exporter_ is a stats exporter that
+exports data to [SignalFx](https://signalfx.com), a real-time monitoring
+solution for cloud and distributed applications. SignalFx ingests that
+data and offers various visualizations on charts, dashboards and service
+maps, as well as real-time anomaly detection.
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you must have a [SignalFx](https://signalfx.com)
+account and corresponding [data ingest
+token](https://docs.signalfx.com/en/latest/admin-guide/tokens.html).
+
+#### Java versions
+
+This exporter requires Java 7 or above.
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-exporter-stats-signalfx</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+
+```
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-stats-signalfx:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+### Register the exporter
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) {
+    // SignalFx token is read from Java system properties.
+    // Stats will be reported every second by default.
+    SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().build());
+  }
+}
+```
+
+If you want to pass in the token yourself, or set a different reporting
+interval, use:
+
+```java
+// Use token "your_signalfx_token" and report every 5 seconds.
+SignalFxStatsExporter.create(
+    SignalFxStatsConfiguration.builder()
+        .setToken("your_signalfx_token")
+        .setExportInterval(Duration.create(5, 0))
+        .build());
+```
diff --git a/exporters/stats/signalfx/build.gradle b/exporters/stats/signalfx/build.gradle
new file mode 100644
index 0000000..d496b1e
--- /dev/null
+++ b/exporters/stats/signalfx/build.gradle
@@ -0,0 +1,23 @@
+description = 'OpenCensus SignalFx Stats Exporter'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.7
+    it.targetCompatibility = 1.7
+}
+
+dependencies {
+    compileOnly libraries.auto_value
+
+    compile project(':opencensus-api'),
+            libraries.guava
+
+    compile (libraries.signalfx_java) {
+        // Prefer library version.
+        exclude group: 'com.google.guava', module: 'guava'
+    }
+
+    testCompile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java
new file mode 100644
index 0000000..5601a54
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java
@@ -0,0 +1,59 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.signalfx.endpoint.SignalFxEndpoint;
+import com.signalfx.metrics.auth.StaticAuthToken;
+import com.signalfx.metrics.connection.HttpDataPointProtobufReceiverFactory;
+import com.signalfx.metrics.connection.HttpEventProtobufReceiverFactory;
+import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
+import com.signalfx.metrics.flush.AggregateMetricSender;
+import java.net.URI;
+import java.util.Collections;
+
+/** Interface for creators of {@link AggregateMetricSender}. */
+interface SignalFxMetricsSenderFactory {
+
+  /**
+   * Creates a new SignalFx metrics sender instance.
+   *
+   * @param endpoint The SignalFx ingest endpoint URL.
+   * @param token The SignalFx ingest token.
+   * @param errorHandler An {@link OnSendErrorHandler} through which errors when sending data to
+   *     SignalFx will be communicated.
+   * @return The created {@link AggregateMetricSender} instance.
+   */
+  AggregateMetricSender create(URI endpoint, String token, OnSendErrorHandler errorHandler);
+
+  /** The default, concrete implementation of this interface. */
+  SignalFxMetricsSenderFactory DEFAULT =
+      new SignalFxMetricsSenderFactory() {
+        @Override
+        @SuppressWarnings("nullness")
+        public AggregateMetricSender create(
+            URI endpoint, String token, OnSendErrorHandler errorHandler) {
+          SignalFxEndpoint sfx =
+              new SignalFxEndpoint(endpoint.getScheme(), endpoint.getHost(), endpoint.getPort());
+          return new AggregateMetricSender(
+              null,
+              new HttpDataPointProtobufReceiverFactory(sfx).setVersion(2),
+              new HttpEventProtobufReceiverFactory(sfx),
+              new StaticAuthToken(token),
+              Collections.singleton(errorHandler));
+        }
+      };
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java
new file mode 100644
index 0000000..2eb75c4
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java
@@ -0,0 +1,188 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Datum;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Dimension;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.MetricType;
+import io.opencensus.common.Function;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Adapter for a {@code ViewData}'s contents into SignalFx datapoints. */
+@SuppressWarnings("deprecation")
+final class SignalFxSessionAdaptor {
+
+  private SignalFxSessionAdaptor() {}
+
+  /**
+   * Converts the given view data into datapoints that can be sent to SignalFx.
+   *
+   * <p>The view name is used as the metric name, and the aggregation type and aggregation window
+   * type determine the metric type.
+   *
+   * @param data The {@link ViewData} containing the aggregation data of each combination of tag
+   *     values.
+   * @return A list of datapoints for the corresponding metric timeseries of this view's metric.
+   */
+  static List<DataPoint> adapt(ViewData data) {
+    View view = data.getView();
+    List<TagKey> keys = view.getColumns();
+
+    MetricType metricType = getMetricTypeForAggregation(view.getAggregation(), view.getWindow());
+    if (metricType == null) {
+      return Collections.emptyList();
+    }
+
+    List<DataPoint> datapoints = new ArrayList<>(data.getAggregationMap().size());
+    for (Map.Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+        data.getAggregationMap().entrySet()) {
+      datapoints.add(
+          DataPoint.newBuilder()
+              .setMetric(view.getName().asString())
+              .setMetricType(metricType)
+              .addAllDimensions(createDimensions(keys, entry.getKey()))
+              .setValue(createDatum(entry.getValue()))
+              .build());
+    }
+    return datapoints;
+  }
+
+  @VisibleForTesting
+  @javax.annotation.Nullable
+  static MetricType getMetricTypeForAggregation(
+      Aggregation aggregation, View.AggregationWindow window) {
+    if (aggregation instanceof Aggregation.Mean || aggregation instanceof Aggregation.LastValue) {
+      return MetricType.GAUGE;
+    } else if (aggregation instanceof Aggregation.Count || aggregation instanceof Aggregation.Sum) {
+      if (window instanceof View.AggregationWindow.Cumulative) {
+        return MetricType.CUMULATIVE_COUNTER;
+      }
+      // TODO(mpetazzoni): support incremental counters when AggregationWindow.Interval is ready
+    }
+
+    // TODO(mpetazzoni): add support for histograms (Aggregation.Distribution).
+    return null;
+  }
+
+  @VisibleForTesting
+  static Iterable<Dimension> createDimensions(
+      List<TagKey> keys, List</*@Nullable*/ TagValue> values) {
+    Preconditions.checkArgument(
+        keys.size() == values.size(), "TagKeys and TagValues don't have the same size.");
+    List<Dimension> dimensions = new ArrayList<>(keys.size());
+    for (ListIterator<TagKey> it = keys.listIterator(); it.hasNext(); ) {
+      TagKey key = it.next();
+      TagValue value = values.get(it.previousIndex());
+      if (value == null || Strings.isNullOrEmpty(value.asString())) {
+        continue;
+      }
+      dimensions.add(createDimension(key, value));
+    }
+    return dimensions;
+  }
+
+  @VisibleForTesting
+  static Dimension createDimension(TagKey key, TagValue value) {
+    return Dimension.newBuilder().setKey(key.getName()).setValue(value.asString()).build();
+  }
+
+  @VisibleForTesting
+  static Datum createDatum(AggregationData data) {
+    final Datum.Builder builder = Datum.newBuilder();
+    data.match(
+        new Function<SumDataDouble, Void>() {
+          @Override
+          public Void apply(SumDataDouble arg) {
+            builder.setDoubleValue(arg.getSum());
+            return null;
+          }
+        },
+        new Function<SumDataLong, Void>() {
+          @Override
+          public Void apply(SumDataLong arg) {
+            builder.setIntValue(arg.getSum());
+            return null;
+          }
+        },
+        new Function<CountData, Void>() {
+          @Override
+          public Void apply(CountData arg) {
+            builder.setIntValue(arg.getCount());
+            return null;
+          }
+        },
+        new Function<DistributionData, Void>() {
+          @Override
+          public Void apply(DistributionData arg) {
+            // TODO(mpetazzoni): add histogram support.
+            throw new IllegalArgumentException("Distribution aggregations are not supported");
+          }
+        },
+        new Function<LastValueDataDouble, Void>() {
+          @Override
+          public Void apply(LastValueDataDouble arg) {
+            builder.setDoubleValue(arg.getLastValue());
+            return null;
+          }
+        },
+        new Function<LastValueDataLong, Void>() {
+          @Override
+          public Void apply(LastValueDataLong arg) {
+            builder.setIntValue(arg.getLastValue());
+            return null;
+          }
+        },
+        new Function<AggregationData, Void>() {
+          @Override
+          public Void apply(AggregationData arg) {
+            // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+            // we need to continue supporting Mean, since it could still be used by users and some
+            // deprecated RPC views.
+            if (arg instanceof AggregationData.MeanData) {
+              builder.setDoubleValue(((AggregationData.MeanData) arg).getMean());
+              return null;
+            }
+            throw new IllegalArgumentException("Unknown Aggregation.");
+          }
+        });
+    return builder.build();
+  }
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java
new file mode 100644
index 0000000..e8b4d75
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java
@@ -0,0 +1,153 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import io.opencensus.common.Duration;
+import java.net.URI;
+import java.net.URISyntaxException;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link SignalFxStatsExporter}.
+ *
+ * @since 0.11
+ */
+@AutoValue
+@Immutable
+public abstract class SignalFxStatsConfiguration {
+
+  /**
+   * The default SignalFx ingest API URL.
+   *
+   * @since 0.11
+   */
+  public static final URI DEFAULT_SIGNALFX_ENDPOINT;
+
+  static {
+    try {
+      DEFAULT_SIGNALFX_ENDPOINT = new URI("https://ingest.signalfx.com");
+    } catch (URISyntaxException e) {
+      // This shouldn't happen if DEFAULT_SIGNALFX_ENDPOINT was typed in correctly.
+      throw new IllegalStateException(e);
+    }
+  }
+
+  /**
+   * The default stats export interval.
+   *
+   * @since 0.11
+   */
+  public static final Duration DEFAULT_EXPORT_INTERVAL = Duration.create(1, 0);
+
+  private static final Duration ZERO = Duration.create(0, 0);
+
+  SignalFxStatsConfiguration() {}
+
+  /**
+   * Returns the SignalFx ingest API URL.
+   *
+   * @return the SignalFx ingest API URL.
+   * @since 0.11
+   */
+  public abstract URI getIngestEndpoint();
+
+  /**
+   * Returns the authentication token.
+   *
+   * @return the authentication token.
+   * @since 0.11
+   */
+  public abstract String getToken();
+
+  /**
+   * Returns the export interval between pushes to SignalFx.
+   *
+   * @return the export interval.
+   * @since 0.11
+   */
+  public abstract Duration getExportInterval();
+
+  /**
+   * Returns a new {@link Builder}.
+   *
+   * @return a {@code Builder}.
+   * @since 0.11
+   */
+  public static Builder builder() {
+    return new AutoValue_SignalFxStatsConfiguration.Builder()
+        .setIngestEndpoint(DEFAULT_SIGNALFX_ENDPOINT)
+        .setExportInterval(DEFAULT_EXPORT_INTERVAL);
+  }
+
+  /**
+   * Builder for {@link SignalFxStatsConfiguration}.
+   *
+   * @since 0.11
+   */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    Builder() {}
+
+    /**
+     * Sets the given SignalFx ingest API URL.
+     *
+     * @param url the SignalFx ingest API URL.
+     * @return this.
+     * @since 0.11
+     */
+    public abstract Builder setIngestEndpoint(URI url);
+
+    /**
+     * Sets the given authentication token.
+     *
+     * @param token the authentication token.
+     * @return this.
+     * @since 0.11
+     */
+    public abstract Builder setToken(String token);
+
+    /**
+     * Sets the export interval.
+     *
+     * @param exportInterval the export interval between pushes to SignalFx.
+     * @return this.
+     * @since 0.11
+     */
+    public abstract Builder setExportInterval(Duration exportInterval);
+
+    abstract SignalFxStatsConfiguration autoBuild();
+
+    /**
+     * Builds a new {@link SignalFxStatsConfiguration} with current settings.
+     *
+     * @return a {@code SignalFxStatsConfiguration}.
+     * @since 0.11
+     */
+    public SignalFxStatsConfiguration build() {
+      SignalFxStatsConfiguration config = autoBuild();
+      Preconditions.checkArgument(
+          !Strings.isNullOrEmpty(config.getToken()), "Invalid SignalFx token");
+      Preconditions.checkArgument(
+          config.getExportInterval().compareTo(ZERO) > 0, "Interval duration must be positive");
+      return config;
+    }
+  }
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java
new file mode 100644
index 0000000..f7915b7
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java
@@ -0,0 +1,109 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.ViewManager;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Exporter to SignalFx.
+ *
+ * <p>Example of usage:
+ *
+ * <pre><code>
+ * public static void main(String[] args) {
+ *   SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().build());
+ *   ... // Do work.
+ * }
+ * </code></pre>
+ *
+ * @since 0.11
+ */
+public final class SignalFxStatsExporter {
+
+  private static final Object monitor = new Object();
+
+  private final SignalFxStatsConfiguration configuration;
+  private final SignalFxStatsExporterWorkerThread workerThread;
+
+  @GuardedBy("monitor")
+  @Nullable
+  private static SignalFxStatsExporter exporter = null;
+
+  private SignalFxStatsExporter(SignalFxStatsConfiguration configuration, ViewManager viewManager) {
+    Preconditions.checkNotNull(configuration, "SignalFx stats exporter configuration");
+    this.configuration = configuration;
+    this.workerThread =
+        new SignalFxStatsExporterWorkerThread(
+            SignalFxMetricsSenderFactory.DEFAULT,
+            configuration.getIngestEndpoint(),
+            configuration.getToken(),
+            configuration.getExportInterval(),
+            viewManager);
+  }
+
+  /**
+   * Creates a SignalFx Stats exporter from the given {@link SignalFxStatsConfiguration}.
+   *
+   * <p>If {@code ingestEndpoint} is not set on the configuration, the exporter will use {@link
+   * SignalFxStatsConfiguration#DEFAULT_SIGNALFX_ENDPOINT}.
+   *
+   * <p>If {@code exportInterval} is not set on the configuration, the exporter will use {@link
+   * SignalFxStatsConfiguration#DEFAULT_EXPORT_INTERVAL}.
+   *
+   * @param configuration the {@code SignalFxStatsConfiguration}.
+   * @throws IllegalStateException if a SignalFx exporter is already created.
+   * @since 0.11
+   */
+  public static void create(SignalFxStatsConfiguration configuration) {
+    synchronized (monitor) {
+      Preconditions.checkState(exporter == null, "SignalFx stats exporter is already created.");
+      exporter = new SignalFxStatsExporter(configuration, Stats.getViewManager());
+      exporter.workerThread.start();
+    }
+  }
+
+  @VisibleForTesting
+  static void unsafeResetExporter() {
+    synchronized (monitor) {
+      if (exporter != null) {
+        SignalFxStatsExporterWorkerThread workerThread = exporter.workerThread;
+        if (workerThread != null && workerThread.isAlive()) {
+          try {
+            workerThread.interrupt();
+            workerThread.join();
+          } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+          }
+        }
+        exporter = null;
+      }
+    }
+  }
+
+  @VisibleForTesting
+  @Nullable
+  static SignalFxStatsConfiguration unsafeGetConfig() {
+    synchronized (monitor) {
+      return exporter != null ? exporter.configuration : null;
+    }
+  }
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java
new file mode 100644
index 0000000..348778e
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java
@@ -0,0 +1,105 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.signalfx.metrics.errorhandler.MetricError;
+import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
+import com.signalfx.metrics.flush.AggregateMetricSender;
+import com.signalfx.metrics.flush.AggregateMetricSender.Session;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Worker {@code Thread} that polls ViewData from the Stats's ViewManager and exports to SignalFx.
+ *
+ * <p>{@code SignalFxStatsExporterWorkerThread} is a daemon {@code Thread}
+ */
+final class SignalFxStatsExporterWorkerThread extends Thread {
+
+  private static final Logger logger =
+      Logger.getLogger(SignalFxStatsExporterWorkerThread.class.getName());
+
+  private static final OnSendErrorHandler ERROR_HANDLER =
+      new OnSendErrorHandler() {
+        @Override
+        public void handleError(MetricError error) {
+          logger.log(Level.WARNING, "Unable to send metrics to SignalFx: {0}", error.getMessage());
+        }
+      };
+
+  private final long intervalMs;
+  private final ViewManager views;
+  private final AggregateMetricSender sender;
+
+  SignalFxStatsExporterWorkerThread(
+      SignalFxMetricsSenderFactory factory,
+      URI endpoint,
+      String token,
+      Duration interval,
+      ViewManager views) {
+    this.intervalMs = interval.toMillis();
+    this.views = views;
+    this.sender = factory.create(endpoint, token, ERROR_HANDLER);
+
+    setDaemon(true);
+    setName(getClass().getSimpleName());
+    logger.log(Level.FINE, "Initialized SignalFx exporter to {0}.", endpoint);
+  }
+
+  @VisibleForTesting
+  void export() throws IOException {
+    Session session = sender.createSession();
+    try {
+      for (View view : views.getAllExportedViews()) {
+        ViewData data = views.getView(view.getName());
+        if (data == null) {
+          continue;
+        }
+
+        for (DataPoint datapoint : SignalFxSessionAdaptor.adapt(data)) {
+          session.setDatapoint(datapoint);
+        }
+      }
+    } finally {
+      session.close();
+    }
+  }
+
+  @Override
+  public void run() {
+    while (true) {
+      try {
+        export();
+        Thread.sleep(intervalMs);
+      } catch (InterruptedException ie) {
+        Thread.currentThread().interrupt();
+        break;
+      } catch (Throwable e) {
+        logger.log(Level.WARNING, "Exception thrown by the SignalFx stats exporter", e);
+      }
+    }
+    logger.log(Level.INFO, "SignalFx stats exporter stopped.");
+  }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java
new file mode 100644
index 0000000..34f4dfa
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java
@@ -0,0 +1,320 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Datum;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Dimension;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.MetricType;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+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;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SignalFxSessionAdaptorTest {
+
+  private static final Duration ONE_SECOND = Duration.create(1, 0);
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Mock private View view;
+
+  @Mock private ViewData viewData;
+
+  @Before
+  public void setUp() {
+    Mockito.when(view.getName()).thenReturn(Name.create("view-name"));
+    Mockito.when(view.getColumns()).thenReturn(ImmutableList.of(TagKey.create("animal")));
+    Mockito.when(viewData.getView()).thenReturn(view);
+  }
+
+  @Test
+  public void checkMetricTypeFromAggregation() {
+    assertNull(SignalFxSessionAdaptor.getMetricTypeForAggregation(null, null));
+    assertNull(
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            null, AggregationWindow.Cumulative.create()));
+    assertEquals(
+        MetricType.GAUGE,
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            Aggregation.Mean.create(), AggregationWindow.Cumulative.create()));
+    assertEquals(
+        MetricType.GAUGE,
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            Aggregation.Mean.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+    assertEquals(
+        MetricType.CUMULATIVE_COUNTER,
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            Aggregation.Count.create(), AggregationWindow.Cumulative.create()));
+    assertEquals(
+        MetricType.CUMULATIVE_COUNTER,
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            Aggregation.Sum.create(), AggregationWindow.Cumulative.create()));
+    assertNull(
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(Aggregation.Count.create(), null));
+    assertNull(SignalFxSessionAdaptor.getMetricTypeForAggregation(Aggregation.Sum.create(), null));
+    assertNull(
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            Aggregation.Count.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+    assertNull(
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            Aggregation.Sum.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+    assertNull(
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            Aggregation.Distribution.create(BucketBoundaries.create(ImmutableList.of(3.15d))),
+            AggregationWindow.Cumulative.create()));
+    assertEquals(
+        MetricType.GAUGE,
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            Aggregation.LastValue.create(), AggregationWindow.Cumulative.create()));
+    assertEquals(
+        MetricType.GAUGE,
+        SignalFxSessionAdaptor.getMetricTypeForAggregation(
+            Aggregation.LastValue.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+  }
+
+  @Test
+  public void createDimensionsWithNonMatchingListSizes() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("don't have the same size");
+    SignalFxSessionAdaptor.createDimensions(
+        ImmutableList.of(TagKey.create("animal"), TagKey.create("color")),
+        ImmutableList.of(TagValue.create("dog")));
+  }
+
+  @Test
+  public void createDimensionsIgnoresEmptyValues() {
+    List<Dimension> dimensions =
+        Lists.newArrayList(
+            SignalFxSessionAdaptor.createDimensions(
+                ImmutableList.of(TagKey.create("animal"), TagKey.create("color")),
+                ImmutableList.of(TagValue.create("dog"), TagValue.create(""))));
+    assertEquals(1, dimensions.size());
+    assertEquals("animal", dimensions.get(0).getKey());
+    assertEquals("dog", dimensions.get(0).getValue());
+  }
+
+  @Test
+  public void createDimension() {
+    Dimension dimension =
+        SignalFxSessionAdaptor.createDimension(TagKey.create("animal"), TagValue.create("dog"));
+    assertEquals("animal", dimension.getKey());
+    assertEquals("dog", dimension.getValue());
+  }
+
+  @Test
+  public void unsupportedAggregationYieldsNoDatapoints() {
+    Mockito.when(view.getAggregation())
+        .thenReturn(
+            Aggregation.Distribution.create(BucketBoundaries.create(ImmutableList.of(3.15d))));
+    Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+    List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+    assertEquals(0, datapoints.size());
+  }
+
+  @Test
+  public void noAggregationDataYieldsNoDatapoints() {
+    Mockito.when(view.getAggregation()).thenReturn(Aggregation.Count.create());
+    Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+    List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+    assertEquals(0, datapoints.size());
+  }
+
+  @Test
+  public void createDatumFromDoubleSum() {
+    SumDataDouble data = SumDataDouble.create(3.15d);
+    Datum datum = SignalFxSessionAdaptor.createDatum(data);
+    assertTrue(datum.hasDoubleValue());
+    assertFalse(datum.hasIntValue());
+    assertFalse(datum.hasStrValue());
+    assertEquals(3.15d, datum.getDoubleValue(), 0d);
+  }
+
+  @Test
+  public void createDatumFromLongSum() {
+    SumDataLong data = SumDataLong.create(42L);
+    Datum datum = SignalFxSessionAdaptor.createDatum(data);
+    assertFalse(datum.hasDoubleValue());
+    assertTrue(datum.hasIntValue());
+    assertFalse(datum.hasStrValue());
+    assertEquals(42L, datum.getIntValue());
+  }
+
+  @Test
+  public void createDatumFromCount() {
+    CountData data = CountData.create(42L);
+    Datum datum = SignalFxSessionAdaptor.createDatum(data);
+    assertFalse(datum.hasDoubleValue());
+    assertTrue(datum.hasIntValue());
+    assertFalse(datum.hasStrValue());
+    assertEquals(42L, datum.getIntValue());
+  }
+
+  @Test
+  public void createDatumFromMean() {
+    MeanData data = MeanData.create(3.15d, 2L);
+    Datum datum = SignalFxSessionAdaptor.createDatum(data);
+    assertTrue(datum.hasDoubleValue());
+    assertFalse(datum.hasIntValue());
+    assertFalse(datum.hasStrValue());
+    assertEquals(3.15d, datum.getDoubleValue(), 0d);
+  }
+
+  @Test
+  public void createDatumFromDistributionThrows() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Distribution aggregations are not supported");
+    SignalFxSessionAdaptor.createDatum(
+        DistributionData.create(5, 2, 0, 10, 40, ImmutableList.of(1L)));
+  }
+
+  @Test
+  public void createDatumFromLastValueDouble() {
+    LastValueDataDouble data = LastValueDataDouble.create(12.2);
+    Datum datum = SignalFxSessionAdaptor.createDatum(data);
+    assertTrue(datum.hasDoubleValue());
+    assertFalse(datum.hasIntValue());
+    assertFalse(datum.hasStrValue());
+    assertEquals(12.2, datum.getDoubleValue(), 0d);
+  }
+
+  @Test
+  public void createDatumFromLastValueLong() {
+    LastValueDataLong data = LastValueDataLong.create(100000);
+    Datum datum = SignalFxSessionAdaptor.createDatum(data);
+    assertFalse(datum.hasDoubleValue());
+    assertTrue(datum.hasIntValue());
+    assertFalse(datum.hasStrValue());
+    assertEquals(100000, datum.getIntValue());
+  }
+
+  @Test
+  public void adaptViewIntoDatapoints() {
+    Map<List<TagValue>, AggregationData> map =
+        ImmutableMap.<List<TagValue>, AggregationData>of(
+            ImmutableList.of(TagValue.create("dog")),
+            SumDataLong.create(2L),
+            ImmutableList.of(TagValue.create("cat")),
+            SumDataLong.create(3L));
+    Mockito.when(viewData.getAggregationMap()).thenReturn(map);
+    Mockito.when(view.getAggregation()).thenReturn(Aggregation.Count.create());
+    Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+
+    List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+    assertEquals(2, datapoints.size());
+    for (DataPoint dp : datapoints) {
+      assertEquals("view-name", dp.getMetric());
+      assertEquals(MetricType.CUMULATIVE_COUNTER, dp.getMetricType());
+      assertEquals(1, dp.getDimensionsCount());
+      assertTrue(dp.hasValue());
+      assertFalse(dp.hasSource());
+
+      Datum datum = dp.getValue();
+      assertTrue(datum.hasIntValue());
+      assertFalse(datum.hasDoubleValue());
+      assertFalse(datum.hasStrValue());
+
+      Dimension dimension = dp.getDimensions(0);
+      assertEquals("animal", dimension.getKey());
+      switch (dimension.getValue()) {
+        case "dog":
+          assertEquals(2L, datum.getIntValue());
+          break;
+        case "cat":
+          assertEquals(3L, datum.getIntValue());
+          break;
+        default:
+          fail("unexpected dimension value");
+      }
+    }
+  }
+
+  @Test
+  public void adaptViewWithEmptyTagValueIntoDatapoints() {
+    Map<List<TagValue>, AggregationData> map =
+        ImmutableMap.<List<TagValue>, AggregationData>of(
+            ImmutableList.of(TagValue.create("dog")),
+            SumDataLong.create(2L),
+            ImmutableList.of(TagValue.create("")),
+            SumDataLong.create(3L));
+    Mockito.when(viewData.getAggregationMap()).thenReturn(map);
+    Mockito.when(view.getAggregation()).thenReturn(Aggregation.Count.create());
+    Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+
+    List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+    assertEquals(2, datapoints.size());
+    for (DataPoint dp : datapoints) {
+      assertEquals("view-name", dp.getMetric());
+      assertEquals(MetricType.CUMULATIVE_COUNTER, dp.getMetricType());
+      assertTrue(dp.hasValue());
+      assertFalse(dp.hasSource());
+
+      Datum datum = dp.getValue();
+      assertTrue(datum.hasIntValue());
+      assertFalse(datum.hasDoubleValue());
+      assertFalse(datum.hasStrValue());
+
+      switch (dp.getDimensionsCount()) {
+        case 0:
+          assertEquals(3L, datum.getIntValue());
+          break;
+        case 1:
+          Dimension dimension = dp.getDimensions(0);
+          assertEquals("animal", dimension.getKey());
+          assertEquals("dog", dimension.getValue());
+          assertEquals(2L, datum.getIntValue());
+          break;
+        default:
+          fail("Unexpected number of dimensions on the created datapoint");
+          break;
+      }
+    }
+  }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java
new file mode 100644
index 0000000..1d3508f
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import static org.junit.Assert.assertEquals;
+
+import io.opencensus.common.Duration;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SignalFxStatsConfiguration}. */
+@RunWith(JUnit4.class)
+public class SignalFxStatsConfigurationTest {
+
+  private static final String TEST_TOKEN = "token";
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void buildWithDefaults() {
+    SignalFxStatsConfiguration configuration =
+        SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build();
+    assertEquals(TEST_TOKEN, configuration.getToken());
+    assertEquals(
+        SignalFxStatsConfiguration.DEFAULT_SIGNALFX_ENDPOINT, configuration.getIngestEndpoint());
+    assertEquals(
+        SignalFxStatsConfiguration.DEFAULT_EXPORT_INTERVAL, configuration.getExportInterval());
+  }
+
+  @Test
+  public void buildWithFields() throws URISyntaxException {
+    URI url = new URI("http://example.com");
+    Duration duration = Duration.create(5, 0);
+    SignalFxStatsConfiguration configuration =
+        SignalFxStatsConfiguration.builder()
+            .setToken(TEST_TOKEN)
+            .setIngestEndpoint(url)
+            .setExportInterval(duration)
+            .build();
+    assertEquals(TEST_TOKEN, configuration.getToken());
+    assertEquals(url, configuration.getIngestEndpoint());
+    assertEquals(duration, configuration.getExportInterval());
+  }
+
+  @Test
+  public void sameConfigurationsAreEqual() {
+    SignalFxStatsConfiguration config1 =
+        SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build();
+    SignalFxStatsConfiguration config2 =
+        SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build();
+    assertEquals(config1, config2);
+    assertEquals(config1.hashCode(), config2.hashCode());
+  }
+
+  @Test
+  public void buildWithEmptyToken() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Invalid SignalFx token");
+    SignalFxStatsConfiguration.builder().setToken("").build();
+  }
+
+  @Test
+  public void buildWithNegativeDuration() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Interval duration must be positive");
+    SignalFxStatsConfiguration.builder()
+        .setToken(TEST_TOKEN)
+        .setExportInterval(Duration.create(-1, 0))
+        .build();
+  }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java
new file mode 100644
index 0000000..cc5730b
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import static org.junit.Assert.assertEquals;
+
+import io.opencensus.common.Duration;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SignalFxStatsExporter}. */
+@RunWith(JUnit4.class)
+public class SignalFxStatsExporterTest {
+
+  private static final String TEST_TOKEN = "token";
+  private static final String TEST_ENDPOINT = "https://example.com";
+  private static final Duration ONE_SECOND = Duration.create(1, 0);
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @After
+  public void tearDown() {
+    SignalFxStatsExporter.unsafeResetExporter();
+  }
+
+  @Test
+  public void createWithNullConfiguration() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("configuration");
+    SignalFxStatsExporter.create(null);
+  }
+
+  @Test
+  public void createWithNullHostUsesDefault() {
+    SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build());
+    assertEquals(
+        SignalFxStatsConfiguration.DEFAULT_SIGNALFX_ENDPOINT,
+        SignalFxStatsExporter.unsafeGetConfig().getIngestEndpoint());
+  }
+
+  @Test
+  public void createWithNullIntervalUsesDefault() {
+    SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build());
+    assertEquals(
+        SignalFxStatsConfiguration.DEFAULT_EXPORT_INTERVAL,
+        SignalFxStatsExporter.unsafeGetConfig().getExportInterval());
+  }
+
+  @Test
+  public void createExporterTwice() {
+    SignalFxStatsConfiguration config =
+        SignalFxStatsConfiguration.builder()
+            .setToken(TEST_TOKEN)
+            .setExportInterval(ONE_SECOND)
+            .build();
+    SignalFxStatsExporter.create(config);
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("SignalFx stats exporter is already created.");
+    SignalFxStatsExporter.create(config);
+  }
+
+  @Test
+  public void createWithConfiguration() throws URISyntaxException {
+    SignalFxStatsConfiguration config =
+        SignalFxStatsConfiguration.builder()
+            .setToken(TEST_TOKEN)
+            .setIngestEndpoint(new URI(TEST_ENDPOINT))
+            .setExportInterval(ONE_SECOND)
+            .build();
+    SignalFxStatsExporter.create(config);
+    assertEquals(config, SignalFxStatsExporter.unsafeGetConfig());
+  }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java
new file mode 100644
index 0000000..d8852d5
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
+import com.signalfx.metrics.flush.AggregateMetricSender;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Datum;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Dimension;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.MetricType;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SignalFxStatsExporterWorkerThreadTest {
+
+  private static final String TEST_TOKEN = "token";
+  private static final Duration ONE_SECOND = Duration.create(1, 0);
+
+  @Mock private AggregateMetricSender.Session session;
+
+  @Mock private ViewManager viewManager;
+
+  @Mock private SignalFxMetricsSenderFactory factory;
+
+  private URI endpoint;
+
+  @Before
+  public void setUp() throws Exception {
+    endpoint = new URI("http://example.com");
+
+    Mockito.when(
+            factory.create(
+                Mockito.any(URI.class), Mockito.anyString(), Mockito.any(OnSendErrorHandler.class)))
+        .thenAnswer(
+            new Answer<AggregateMetricSender>() {
+              @Override
+              public AggregateMetricSender answer(InvocationOnMock invocation) {
+                Object[] args = invocation.getArguments();
+                AggregateMetricSender sender =
+                    SignalFxMetricsSenderFactory.DEFAULT.create(
+                        (URI) args[0], (String) args[1], (OnSendErrorHandler) args[2]);
+                AggregateMetricSender spy = Mockito.spy(sender);
+                Mockito.doReturn(session).when(spy).createSession();
+                return spy;
+              }
+            });
+  }
+
+  @Test
+  public void createThread() {
+    SignalFxStatsExporterWorkerThread thread =
+        new SignalFxStatsExporterWorkerThread(
+            factory, endpoint, TEST_TOKEN, ONE_SECOND, viewManager);
+    assertTrue(thread.isDaemon());
+    assertThat(thread.getName(), startsWith("SignalFx"));
+  }
+
+  @Test
+  public void senderThreadInterruptStopsLoop() throws InterruptedException {
+    Mockito.when(session.setDatapoint(Mockito.any(DataPoint.class))).thenReturn(session);
+    Mockito.when(viewManager.getAllExportedViews()).thenReturn(ImmutableSet.<View>of());
+
+    SignalFxStatsExporterWorkerThread thread =
+        new SignalFxStatsExporterWorkerThread(
+            factory, endpoint, TEST_TOKEN, ONE_SECOND, viewManager);
+    thread.start();
+    thread.interrupt();
+    thread.join(5000, 0);
+    assertFalse("Worker thread should have stopped", thread.isAlive());
+  }
+
+  @Test
+  public void setsDatapointsFromViewOnSession() throws IOException {
+    View view = Mockito.mock(View.class);
+    Name viewName = Name.create("test");
+    Mockito.when(view.getName()).thenReturn(viewName);
+    Mockito.when(view.getAggregation()).thenReturn(Aggregation.Mean.create());
+    Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+    Mockito.when(view.getColumns()).thenReturn(ImmutableList.of(TagKey.create("animal")));
+
+    ViewData viewData = Mockito.mock(ViewData.class);
+    Mockito.when(viewData.getView()).thenReturn(view);
+    Mockito.when(viewData.getAggregationMap())
+        .thenReturn(
+            ImmutableMap.<List<TagValue>, AggregationData>of(
+                ImmutableList.of(TagValue.create("cat")), MeanData.create(3.15d, 1)));
+
+    Mockito.when(viewManager.getAllExportedViews()).thenReturn(ImmutableSet.of(view));
+    Mockito.when(viewManager.getView(Mockito.eq(viewName))).thenReturn(viewData);
+
+    SignalFxStatsExporterWorkerThread thread =
+        new SignalFxStatsExporterWorkerThread(
+            factory, endpoint, TEST_TOKEN, ONE_SECOND, viewManager);
+    thread.export();
+
+    DataPoint datapoint =
+        DataPoint.newBuilder()
+            .setMetric("test")
+            .setMetricType(MetricType.GAUGE)
+            .addDimensions(Dimension.newBuilder().setKey("animal").setValue("cat").build())
+            .setValue(Datum.newBuilder().setDoubleValue(3.15d).build())
+            .build();
+    Mockito.verify(session).setDatapoint(Mockito.eq(datapoint));
+    Mockito.verify(session).close();
+  }
+}
diff --git a/exporters/stats/stackdriver/README.md b/exporters/stats/stackdriver/README.md
new file mode 100644
index 0000000..1b35c63
--- /dev/null
+++ b/exporters/stats/stackdriver/README.md
@@ -0,0 +1,171 @@
+# OpenCensus Stackdriver Stats Exporter
+
+The *OpenCensus Stackdriver Stats Exporter* is a stats exporter that exports data to 
+Stackdriver Monitoring. [Stackdriver Monitoring][stackdriver-monitoring] provides visibility into 
+the performance, uptime, and overall health of cloud-powered applications. Stackdriver ingests that 
+data and generates insights via dashboards, charts, and alerts.
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you must have an application that you'd like to monitor. The app can be on 
+Google Cloud Platform, on-premise, or another cloud platform.
+
+In order to be able to push your stats to [Stackdriver Monitoring][stackdriver-monitoring], you must:
+
+1. [Create a Cloud project](https://support.google.com/cloud/answer/6251787?hl=en).
+2. [Enable billing](https://support.google.com/cloud/answer/6288653#new-billing).
+3. [Enable the Stackdriver Monitoring API](https://console.cloud.google.com/apis/dashboard).
+
+These steps enable the API but don't require that your app is hosted on Google Cloud Platform.
+
+### Hello "Stackdriver Stats"
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-exporter-stats-stackdriver</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-stats-stackdriver:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This uses the default configuration for authentication and a given project ID.
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) {
+    StackdriverStatsExporter.createAndRegister(
+        StackdriverStatsConfiguration.builder().build());
+  }
+}
+```
+
+#### Set Monitored Resource for exporter
+
+By default, Stackdriver Stats Exporter will try to automatically detect the environment if your 
+application is running on GCE, GKE or AWS EC2, and generate a corresponding Stackdriver GCE/GKE/EC2 
+monitored resource. For GKE particularly, you may want to set up some environment variables so that 
+Exporter can correctly identify your pod, cluster and container. Follow the Kubernetes instruction 
+[here](https://cloud.google.com/kubernetes-engine/docs/tutorials/custom-metrics-autoscaling#exporting_metrics_from_the_application) 
+and [here](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/).
+
+Otherwise, Exporter will use [a global Stackdriver monitored resource with a project_id label](https://cloud.google.com/monitoring/api/resources#tag_global), 
+and it works fine when you have only one exporter running. 
+
+If you want to have multiple processes exporting stats for the same metric concurrently, and your 
+application is running on some different environment than GCE, GKE or AWS EC2 (for example DataFlow), 
+please associate a unique monitored resource with each exporter if possible. 
+Please note that there is also an "opencensus_task" metric label that uniquely identifies the 
+uploaded stats.
+
+To set a custom MonitoredResource:
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) {
+    // A sample DataFlow monitored resource.
+    MonitoredResource myResource = MonitoredResource.newBuilder()
+                                               .setType("dataflow_job")
+                                               .putLabels("project_id", "my_project")
+                                               .putLabels("job_name", "my_job")
+                                               .putLabels("region", "us-east1")
+                                               .build();
+    
+    // Set a custom MonitoredResource. Please make sure each Stackdriver Stats Exporter has a 
+    // unique MonitoredResource.      
+    StackdriverStatsExporter.createAndRegister(
+        StackdriverStatsConfiguration.builder().setMonitoredResource(myResource).build());
+  }
+}
+```
+
+For a complete list of valid Stackdriver monitored resources, please refer to [Stackdriver 
+Documentation](https://cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource).
+Please also note that although there are a lot of monitored resources available on [Stackdriver](https://cloud.google.com/monitoring/api/resources), 
+only [a small subset of them](https://cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource) 
+are compatible with the Opencensus Stackdriver Stats Exporter.
+
+#### Authentication
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the authentication see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#authentication).
+
+If you prefer to manually set the credentials use:
+```
+StackdriverStatsExporter.createAndRegister(
+    StackdriverStatsConfiguration.builder()
+        .setCredentials(new GoogleCredentials(new AccessToken(accessToken, expirationTime)))
+        .setProjectId("MyStackdriverProjectId")
+        .setExportInterval(Duration.create(10, 0))
+        .build());
+```
+
+#### Specifying a Project ID
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the project ID see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id).
+
+If you prefer to manually set the project ID use:
+```
+StackdriverStatsExporter.createAndRegister(
+    StackdriverStatsConfiguration.builder().setProjectId("MyStackdriverProjectId").build());
+```
+
+#### Java Versions
+
+Java 7 or above is required for using this exporter.
+
+## FAQ
+### Why did I get a PERMISSION_DENIED error from Stackdriver when using this exporter?
+To use our Stackdriver Stats exporter, you need to set up billing for your cloud project, since
+creating and uploading custom metrics to Stackdriver Monitoring is
+[not free](https://cloud.google.com/stackdriver/pricing_v2#monitoring-costs).
+
+To enable billing, follow the instructions [here](https://support.google.com/cloud/answer/6288653#new-billing).
+
+### What is "opencensus_task" metric label ?
+Stackdriver requires that each Timeseries to be updated only by one task at a time. A
+`Timeseries` is uniquely identified by the `MonitoredResource` and the `Metric`'s labels.
+Stackdriver exporter adds a new `Metric` label for each custom `Metric` to ensure the uniqueness
+of the `Timeseries`. The format of the label is: `{LANGUAGE}-{PID}@{HOSTNAME}`, if `{PID}` is not
+available a random number will be used.
+
+### Why did I get an error "java.lang.NoSuchMethodError: com.google.common...", like "java.lang.NoSuchMethodError:com.google.common.base.Throwables.throwIfInstanceOf"?
+This is probably because there is a version conflict on Guava in the dependency tree.
+
+For example, `com.google.common.base.Throwables.throwIfInstanceOf` is introduced to Guava 20.0.
+If your application has a dependency that bundles a Guava with version 19.0 or below
+(for example, gRPC 1.10.0), it might cause a `NoSuchMethodError` since
+`com.google.common.base.Throwables.throwIfInstanceOf` doesn't exist before Guava 20.0.
+
+In this case, please either add an explicit dependency on a newer version of Guava that has the 
+new method (20.0 in the previous example), or if possible, upgrade the dependency that depends on 
+Guava to a newer version that depends on the newer Guava (for example, upgrade to gRPC 1.12.0).
+
+[stackdriver-monitoring]: https://cloud.google.com/monitoring/
diff --git a/exporters/stats/stackdriver/build.gradle b/exporters/stats/stackdriver/build.gradle
new file mode 100644
index 0000000..0bc302a
--- /dev/null
+++ b/exporters/stats/stackdriver/build.gradle
@@ -0,0 +1,30 @@
+description = 'OpenCensus Stats Stackdriver Exporter'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.7
+    it.targetCompatibility = 1.7
+}
+
+dependencies {
+    compileOnly libraries.auto_value
+
+    compile project(':opencensus-api'),
+            project(':opencensus-contrib-monitored-resource-util'),
+            libraries.google_auth,
+            libraries.guava
+
+    compile (libraries.google_cloud_monitoring) {
+        // Prefer library version.
+        exclude group: 'com.google.guava', module: 'guava'
+
+        // Prefer library version.
+        exclude group: 'com.google.code.findbugs', module: 'jsr305'
+
+        // We will always be more up to date.
+        exclude group: 'io.opencensus', module: 'opencensus-api'
+    }
+
+    testCompile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
\ No newline at end of file
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java
new file mode 100644
index 0000000..4f8715b
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java
@@ -0,0 +1,518 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.api.Distribution;
+import com.google.api.Distribution.BucketOptions;
+import com.google.api.Distribution.BucketOptions.Explicit;
+import com.google.api.LabelDescriptor;
+import com.google.api.LabelDescriptor.ValueType;
+import com.google.api.Metric;
+import com.google.api.MetricDescriptor;
+import com.google.api.MetricDescriptor.MetricKind;
+import com.google.api.MonitoredResource;
+import com.google.cloud.MetadataConfig;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.monitoring.v3.Point;
+import com.google.monitoring.v3.TimeInterval;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.monitoring.v3.TypedValue;
+import com.google.monitoring.v3.TypedValue.Builder;
+import com.google.protobuf.Timestamp;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResourceUtils;
+import io.opencensus.contrib.monitoredresource.util.ResourceType;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Util methods to convert OpenCensus Stats data models to StackDriver monitoring data models. */
+@SuppressWarnings("deprecation")
+final class StackdriverExportUtils {
+
+  // TODO(songya): do we want these constants to be customizable?
+  @VisibleForTesting static final String LABEL_DESCRIPTION = "OpenCensus TagKey";
+  @VisibleForTesting static final String OPENCENSUS_TASK = "opencensus_task";
+  @VisibleForTesting static final String OPENCENSUS_TASK_DESCRIPTION = "Opencensus task identifier";
+  private static final String GCP_GKE_CONTAINER = "k8s_container";
+  private static final String GCP_GCE_INSTANCE = "gce_instance";
+  private static final String AWS_EC2_INSTANCE = "aws_ec2_instance";
+  private static final String GLOBAL = "global";
+
+  private static final Logger logger = Logger.getLogger(StackdriverExportUtils.class.getName());
+  private static final String OPENCENSUS_TASK_VALUE_DEFAULT = generateDefaultTaskValue();
+  private static final String PROJECT_ID_LABEL_KEY = "project_id";
+
+  // Constant functions for ValueType.
+  private static final Function<Object, MetricDescriptor.ValueType> VALUE_TYPE_DOUBLE_FUNCTION =
+      Functions.returnConstant(MetricDescriptor.ValueType.DOUBLE);
+  private static final Function<Object, MetricDescriptor.ValueType> VALUE_TYPE_INT64_FUNCTION =
+      Functions.returnConstant(MetricDescriptor.ValueType.INT64);
+  private static final Function<Object, MetricDescriptor.ValueType>
+      VALUE_TYPE_UNRECOGNIZED_FUNCTION =
+          Functions.returnConstant(MetricDescriptor.ValueType.UNRECOGNIZED);
+  private static final Function<Object, MetricDescriptor.ValueType>
+      VALUE_TYPE_DISTRIBUTION_FUNCTION =
+          Functions.returnConstant(MetricDescriptor.ValueType.DISTRIBUTION);
+  private static final Function<Aggregation, MetricDescriptor.ValueType> valueTypeMeanFunction =
+      new Function<Aggregation, MetricDescriptor.ValueType>() {
+        @Override
+        public MetricDescriptor.ValueType apply(Aggregation arg) {
+          // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+          // we need to continue supporting Mean, since it could still be used by users and some
+          // deprecated RPC views.
+          if (arg instanceof Aggregation.Mean) {
+            return MetricDescriptor.ValueType.DOUBLE;
+          }
+          return MetricDescriptor.ValueType.UNRECOGNIZED;
+        }
+      };
+
+  // Constant functions for MetricKind.
+  private static final Function<Object, MetricKind> METRIC_KIND_CUMULATIVE_FUNCTION =
+      Functions.returnConstant(MetricKind.CUMULATIVE);
+  private static final Function<Object, MetricKind> METRIC_KIND_UNRECOGNIZED_FUNCTION =
+      Functions.returnConstant(MetricKind.UNRECOGNIZED);
+
+  // Constant functions for TypedValue.
+  private static final Function<SumDataDouble, TypedValue> typedValueSumDoubleFunction =
+      new Function<SumDataDouble, TypedValue>() {
+        @Override
+        public TypedValue apply(SumDataDouble arg) {
+          Builder builder = TypedValue.newBuilder();
+          builder.setDoubleValue(arg.getSum());
+          return builder.build();
+        }
+      };
+  private static final Function<SumDataLong, TypedValue> typedValueSumLongFunction =
+      new Function<SumDataLong, TypedValue>() {
+        @Override
+        public TypedValue apply(SumDataLong arg) {
+          Builder builder = TypedValue.newBuilder();
+          builder.setInt64Value(arg.getSum());
+          return builder.build();
+        }
+      };
+  private static final Function<CountData, TypedValue> typedValueCountFunction =
+      new Function<CountData, TypedValue>() {
+        @Override
+        public TypedValue apply(CountData arg) {
+          Builder builder = TypedValue.newBuilder();
+          builder.setInt64Value(arg.getCount());
+          return builder.build();
+        }
+      };
+  private static final Function<LastValueDataDouble, TypedValue> typedValueLastValueDoubleFunction =
+      new Function<LastValueDataDouble, TypedValue>() {
+        @Override
+        public TypedValue apply(LastValueDataDouble arg) {
+          Builder builder = TypedValue.newBuilder();
+          builder.setDoubleValue(arg.getLastValue());
+          return builder.build();
+        }
+      };
+  private static final Function<LastValueDataLong, TypedValue> typedValueLastValueLongFunction =
+      new Function<LastValueDataLong, TypedValue>() {
+        @Override
+        public TypedValue apply(LastValueDataLong arg) {
+          Builder builder = TypedValue.newBuilder();
+          builder.setInt64Value(arg.getLastValue());
+          return builder.build();
+        }
+      };
+  private static final Function<AggregationData, TypedValue> typedValueMeanFunction =
+      new Function<AggregationData, TypedValue>() {
+        @Override
+        public TypedValue apply(AggregationData arg) {
+          Builder builder = TypedValue.newBuilder();
+          // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+          // we need to continue supporting Mean, since it could still be used by users and some
+          // deprecated RPC views.
+          if (arg instanceof AggregationData.MeanData) {
+            builder.setDoubleValue(((AggregationData.MeanData) arg).getMean());
+            return builder.build();
+          }
+          throw new IllegalArgumentException("Unknown Aggregation");
+        }
+      };
+
+  private static String generateDefaultTaskValue() {
+    // Something like '<pid>@<hostname>', at least in Oracle and OpenJdk JVMs
+    final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
+    // If not the expected format then generate a random number.
+    if (jvmName.indexOf('@') < 1) {
+      String hostname = "localhost";
+      try {
+        hostname = InetAddress.getLocalHost().getHostName();
+      } catch (UnknownHostException e) {
+        logger.log(Level.INFO, "Unable to get the hostname.", e);
+      }
+      // Generate a random number and use the same format "random_number@hostname".
+      return "java-" + new SecureRandom().nextInt() + "@" + hostname;
+    }
+    return "java-" + jvmName;
+  }
+
+  // Construct a MetricDescriptor using a View.
+  @javax.annotation.Nullable
+  static MetricDescriptor createMetricDescriptor(
+      View view, String projectId, String domain, String displayNamePrefix) {
+    if (!(view.getWindow() instanceof View.AggregationWindow.Cumulative)) {
+      // TODO(songya): Only Cumulative view will be exported to Stackdriver in this version.
+      return null;
+    }
+
+    MetricDescriptor.Builder builder = MetricDescriptor.newBuilder();
+    String viewName = view.getName().asString();
+    String type = generateType(viewName, domain);
+    // Name format refers to
+    // cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.metricDescriptors/create
+    builder.setName(String.format("projects/%s/metricDescriptors/%s", projectId, type));
+    builder.setType(type);
+    builder.setDescription(view.getDescription());
+    String displayName = createDisplayName(viewName, displayNamePrefix);
+    builder.setDisplayName(displayName);
+    for (TagKey tagKey : view.getColumns()) {
+      builder.addLabels(createLabelDescriptor(tagKey));
+    }
+    builder.addLabels(
+        LabelDescriptor.newBuilder()
+            .setKey(OPENCENSUS_TASK)
+            .setDescription(OPENCENSUS_TASK_DESCRIPTION)
+            .setValueType(ValueType.STRING)
+            .build());
+    builder.setUnit(createUnit(view.getAggregation(), view.getMeasure()));
+    builder.setMetricKind(createMetricKind(view.getWindow(), view.getAggregation()));
+    builder.setValueType(createValueType(view.getAggregation(), view.getMeasure()));
+    return builder.build();
+  }
+
+  private static String generateType(String viewName, String domain) {
+    return domain + viewName;
+  }
+
+  private static String createDisplayName(String viewName, String displayNamePrefix) {
+    return displayNamePrefix + viewName;
+  }
+
+  // Construct a LabelDescriptor from a TagKey
+  @VisibleForTesting
+  static LabelDescriptor createLabelDescriptor(TagKey tagKey) {
+    LabelDescriptor.Builder builder = LabelDescriptor.newBuilder();
+    builder.setKey(tagKey.getName());
+    builder.setDescription(LABEL_DESCRIPTION);
+    // Now we only support String tags
+    builder.setValueType(ValueType.STRING);
+    return builder.build();
+  }
+
+  // Construct a MetricKind from an AggregationWindow
+  @VisibleForTesting
+  static MetricKind createMetricKind(View.AggregationWindow window, Aggregation aggregation) {
+    if (aggregation instanceof LastValue) {
+      return MetricKind.GAUGE;
+    }
+    return window.match(
+        METRIC_KIND_CUMULATIVE_FUNCTION, // Cumulative
+        // TODO(songya): We don't support exporting Interval stats to StackDriver in this version.
+        METRIC_KIND_UNRECOGNIZED_FUNCTION, // Interval
+        METRIC_KIND_UNRECOGNIZED_FUNCTION);
+  }
+
+  // Construct a MetricDescriptor.ValueType from an Aggregation and a Measure
+  @VisibleForTesting
+  static String createUnit(Aggregation aggregation, final Measure measure) {
+    if (aggregation instanceof Aggregation.Count) {
+      return "1";
+    }
+    return measure.getUnit();
+  }
+
+  // Construct a MetricDescriptor.ValueType from an Aggregation and a Measure
+  @VisibleForTesting
+  static MetricDescriptor.ValueType createValueType(
+      Aggregation aggregation, final Measure measure) {
+    return aggregation.match(
+        Functions.returnConstant(
+            measure.match(
+                VALUE_TYPE_DOUBLE_FUNCTION, // Sum Double
+                VALUE_TYPE_INT64_FUNCTION, // Sum Long
+                VALUE_TYPE_UNRECOGNIZED_FUNCTION)),
+        VALUE_TYPE_INT64_FUNCTION, // Count
+        VALUE_TYPE_DISTRIBUTION_FUNCTION, // Distribution
+        Functions.returnConstant(
+            measure.match(
+                VALUE_TYPE_DOUBLE_FUNCTION, // LastValue Double
+                VALUE_TYPE_INT64_FUNCTION, // LastValue Long
+                VALUE_TYPE_UNRECOGNIZED_FUNCTION)),
+        valueTypeMeanFunction);
+  }
+
+  // Convert ViewData to a list of TimeSeries, so that ViewData can be uploaded to Stackdriver.
+  static List<TimeSeries> createTimeSeriesList(
+      @javax.annotation.Nullable ViewData viewData,
+      MonitoredResource monitoredResource,
+      String domain) {
+    List<TimeSeries> timeSeriesList = Lists.newArrayList();
+    if (viewData == null) {
+      return timeSeriesList;
+    }
+    View view = viewData.getView();
+    if (!(view.getWindow() instanceof View.AggregationWindow.Cumulative)) {
+      // TODO(songya): Only Cumulative view will be exported to Stackdriver in this version.
+      return timeSeriesList;
+    }
+
+    // Shared fields for all TimeSeries generated from the same ViewData
+    TimeSeries.Builder shared = TimeSeries.newBuilder();
+    shared.setMetricKind(createMetricKind(view.getWindow(), view.getAggregation()));
+    shared.setResource(monitoredResource);
+    shared.setValueType(createValueType(view.getAggregation(), view.getMeasure()));
+
+    // Each entry in AggregationMap will be converted into an independent TimeSeries object
+    for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+        viewData.getAggregationMap().entrySet()) {
+      TimeSeries.Builder builder = shared.clone();
+      builder.setMetric(createMetric(view, entry.getKey(), domain));
+      builder.addPoints(
+          createPoint(entry.getValue(), viewData.getWindowData(), view.getAggregation()));
+      timeSeriesList.add(builder.build());
+    }
+
+    return timeSeriesList;
+  }
+
+  // Create a Metric using the TagKeys and TagValues.
+  @VisibleForTesting
+  static Metric createMetric(View view, List</*@Nullable*/ TagValue> tagValues, String domain) {
+    Metric.Builder builder = Metric.newBuilder();
+    // TODO(songya): use pre-defined metrics for canonical views
+    builder.setType(generateType(view.getName().asString(), domain));
+    Map<String, String> stringTagMap = Maps.newHashMap();
+    List<TagKey> columns = view.getColumns();
+    checkArgument(
+        tagValues.size() == columns.size(), "TagKeys and TagValues don't have same size.");
+    for (int i = 0; i < tagValues.size(); i++) {
+      TagKey key = columns.get(i);
+      TagValue value = tagValues.get(i);
+      if (value == null) {
+        continue;
+      }
+      stringTagMap.put(key.getName(), value.asString());
+    }
+    stringTagMap.put(OPENCENSUS_TASK, OPENCENSUS_TASK_VALUE_DEFAULT);
+    builder.putAllLabels(stringTagMap);
+    return builder.build();
+  }
+
+  // Create Point from AggregationData, AggregationWindowData and Aggregation.
+  @VisibleForTesting
+  static Point createPoint(
+      AggregationData aggregationData,
+      ViewData.AggregationWindowData windowData,
+      Aggregation aggregation) {
+    Point.Builder builder = Point.newBuilder();
+    builder.setInterval(createTimeInterval(windowData, aggregation));
+    builder.setValue(createTypedValue(aggregation, aggregationData));
+    return builder.build();
+  }
+
+  // Convert AggregationWindowData to TimeInterval, currently only support CumulativeData.
+  @VisibleForTesting
+  static TimeInterval createTimeInterval(
+      ViewData.AggregationWindowData windowData, final Aggregation aggregation) {
+    return windowData.match(
+        new Function<ViewData.AggregationWindowData.CumulativeData, TimeInterval>() {
+          @Override
+          public TimeInterval apply(ViewData.AggregationWindowData.CumulativeData arg) {
+            TimeInterval.Builder builder = TimeInterval.newBuilder();
+            builder.setEndTime(convertTimestamp(arg.getEnd()));
+            if (!(aggregation instanceof LastValue)) {
+              builder.setStartTime(convertTimestamp(arg.getStart()));
+            }
+            return builder.build();
+          }
+        },
+        Functions.<TimeInterval>throwIllegalArgumentException(),
+        Functions.<TimeInterval>throwIllegalArgumentException());
+  }
+
+  // Create a TypedValue using AggregationData and Aggregation
+  // Note TypedValue is "A single strongly-typed value", i.e only one field should be set.
+  @VisibleForTesting
+  static TypedValue createTypedValue(
+      final Aggregation aggregation, AggregationData aggregationData) {
+    return aggregationData.match(
+        typedValueSumDoubleFunction,
+        typedValueSumLongFunction,
+        typedValueCountFunction,
+        new Function<DistributionData, TypedValue>() {
+          @Override
+          public TypedValue apply(DistributionData arg) {
+            TypedValue.Builder builder = TypedValue.newBuilder();
+            checkArgument(
+                aggregation instanceof Aggregation.Distribution,
+                "Aggregation and AggregationData mismatch.");
+            builder.setDistributionValue(
+                createDistribution(
+                    arg, ((Aggregation.Distribution) aggregation).getBucketBoundaries()));
+            return builder.build();
+          }
+        },
+        typedValueLastValueDoubleFunction,
+        typedValueLastValueLongFunction,
+        typedValueMeanFunction);
+  }
+
+  // Create a StackDriver Distribution from DistributionData and BucketBoundaries
+  @VisibleForTesting
+  static Distribution createDistribution(
+      DistributionData distributionData, BucketBoundaries bucketBoundaries) {
+    return Distribution.newBuilder()
+        .setBucketOptions(createBucketOptions(bucketBoundaries))
+        .addAllBucketCounts(distributionData.getBucketCounts())
+        .setCount(distributionData.getCount())
+        .setMean(distributionData.getMean())
+        // TODO(songya): uncomment this once Stackdriver supports setting max and min.
+        // .setRange(
+        //    Range.newBuilder()
+        //        .setMax(distributionData.getMax())
+        //        .setMin(distributionData.getMin())
+        //        .build())
+        .setSumOfSquaredDeviation(distributionData.getSumOfSquaredDeviations())
+        .build();
+  }
+
+  // Create BucketOptions from BucketBoundaries
+  @VisibleForTesting
+  static BucketOptions createBucketOptions(BucketBoundaries bucketBoundaries) {
+    return BucketOptions.newBuilder()
+        .setExplicitBuckets(Explicit.newBuilder().addAllBounds(bucketBoundaries.getBoundaries()))
+        .build();
+  }
+
+  // Convert a Census Timestamp to a StackDriver Timestamp
+  @VisibleForTesting
+  static Timestamp convertTimestamp(io.opencensus.common.Timestamp censusTimestamp) {
+    if (censusTimestamp.getSeconds() < 0) {
+      // Stackdriver doesn't handle negative timestamps.
+      return Timestamp.newBuilder().build();
+    }
+    return Timestamp.newBuilder()
+        .setSeconds(censusTimestamp.getSeconds())
+        .setNanos(censusTimestamp.getNanos())
+        .build();
+  }
+
+  /* Return a self-configured Stackdriver monitored resource. */
+  static MonitoredResource getDefaultResource() {
+    MonitoredResource.Builder builder = MonitoredResource.newBuilder();
+    io.opencensus.contrib.monitoredresource.util.MonitoredResource autoDetectedResource =
+        MonitoredResourceUtils.getDefaultResource();
+    if (autoDetectedResource == null) {
+      builder.setType(GLOBAL);
+      if (MetadataConfig.getProjectId() != null) {
+        // For default global resource, always use the project id from MetadataConfig. This allows
+        // stats from other projects (e.g from GAE running in another project) to be collected.
+        builder.putLabels(PROJECT_ID_LABEL_KEY, MetadataConfig.getProjectId());
+      }
+      return builder.build();
+    }
+    builder.setType(mapToStackdriverResourceType(autoDetectedResource.getResourceType()));
+    setMonitoredResourceLabelsForBuilder(builder, autoDetectedResource);
+    return builder.build();
+  }
+
+  private static String mapToStackdriverResourceType(ResourceType resourceType) {
+    switch (resourceType) {
+      case GCP_GCE_INSTANCE:
+        return GCP_GCE_INSTANCE;
+      case GCP_GKE_CONTAINER:
+        return GCP_GKE_CONTAINER;
+      case AWS_EC2_INSTANCE:
+        return AWS_EC2_INSTANCE;
+    }
+    throw new IllegalArgumentException("Unknown resource type.");
+  }
+
+  private static void setMonitoredResourceLabelsForBuilder(
+      MonitoredResource.Builder builder,
+      io.opencensus.contrib.monitoredresource.util.MonitoredResource autoDetectedResource) {
+    switch (autoDetectedResource.getResourceType()) {
+      case GCP_GCE_INSTANCE:
+        GcpGceInstanceMonitoredResource gcpGceInstanceMonitoredResource =
+            (GcpGceInstanceMonitoredResource) autoDetectedResource;
+        builder.putLabels(PROJECT_ID_LABEL_KEY, gcpGceInstanceMonitoredResource.getAccount());
+        builder.putLabels("instance_id", gcpGceInstanceMonitoredResource.getInstanceId());
+        builder.putLabels("zone", gcpGceInstanceMonitoredResource.getZone());
+        return;
+      case GCP_GKE_CONTAINER:
+        GcpGkeContainerMonitoredResource gcpGkeContainerMonitoredResource =
+            (GcpGkeContainerMonitoredResource) autoDetectedResource;
+        builder.putLabels(PROJECT_ID_LABEL_KEY, gcpGkeContainerMonitoredResource.getAccount());
+        builder.putLabels("cluster_name", gcpGkeContainerMonitoredResource.getClusterName());
+        builder.putLabels("container_name", gcpGkeContainerMonitoredResource.getContainerName());
+        builder.putLabels("namespace_name", gcpGkeContainerMonitoredResource.getNamespaceId());
+        builder.putLabels("pod_name", gcpGkeContainerMonitoredResource.getPodId());
+        builder.putLabels("location", gcpGkeContainerMonitoredResource.getZone());
+        return;
+      case AWS_EC2_INSTANCE:
+        AwsEc2InstanceMonitoredResource awsEc2InstanceMonitoredResource =
+            (AwsEc2InstanceMonitoredResource) autoDetectedResource;
+        builder.putLabels("aws_account", awsEc2InstanceMonitoredResource.getAccount());
+        builder.putLabels("instance_id", awsEc2InstanceMonitoredResource.getInstanceId());
+        builder.putLabels("region", "aws:" + awsEc2InstanceMonitoredResource.getRegion());
+        return;
+    }
+    throw new IllegalArgumentException("Unknown subclass of MonitoredResource.");
+  }
+
+  private StackdriverExportUtils() {}
+}
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java
new file mode 100644
index 0000000..5ffed9d
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java
@@ -0,0 +1,274 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import com.google.api.MetricDescriptor;
+import com.google.api.MonitoredResource;
+import com.google.api.gax.rpc.ApiException;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.monitoring.v3.CreateMetricDescriptorRequest;
+import com.google.monitoring.v3.CreateTimeSeriesRequest;
+import com.google.monitoring.v3.ProjectName;
+import com.google.monitoring.v3.TimeSeries;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Scope;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Worker {@code Runnable} that polls ViewData from Stats library and batch export to StackDriver.
+ *
+ * <p>{@code StackdriverExporterWorker} will be started in a daemon {@code Thread}.
+ *
+ * <p>The state of this class should only be accessed from the thread which {@link
+ * StackdriverExporterWorker} resides in.
+ */
+@NotThreadSafe
+final class StackdriverExporterWorker implements Runnable {
+
+  private static final Logger logger = Logger.getLogger(StackdriverExporterWorker.class.getName());
+
+  // Stackdriver Monitoring v3 only accepts up to 200 TimeSeries per CreateTimeSeries call.
+  @VisibleForTesting static final int MAX_BATCH_EXPORT_SIZE = 200;
+
+  @VisibleForTesting static final String DEFAULT_DISPLAY_NAME_PREFIX = "OpenCensus/";
+  @VisibleForTesting static final String CUSTOM_METRIC_DOMAIN = "custom.googleapis.com/";
+
+  @VisibleForTesting
+  static final String CUSTOM_OPENCENSUS_DOMAIN = CUSTOM_METRIC_DOMAIN + "opencensus/";
+
+  private final long scheduleDelayMillis;
+  private final String projectId;
+  private final ProjectName projectName;
+  private final MetricServiceClient metricServiceClient;
+  private final ViewManager viewManager;
+  private final MonitoredResource monitoredResource;
+  private final String domain;
+  private final String displayNamePrefix;
+  private final Map<View.Name, View> registeredViews = new HashMap<View.Name, View>();
+
+  private static final Tracer tracer = Tracing.getTracer();
+  private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+
+  StackdriverExporterWorker(
+      String projectId,
+      MetricServiceClient metricServiceClient,
+      Duration exportInterval,
+      ViewManager viewManager,
+      MonitoredResource monitoredResource,
+      @javax.annotation.Nullable String metricNamePrefix) {
+    this.scheduleDelayMillis = exportInterval.toMillis();
+    this.projectId = projectId;
+    projectName = ProjectName.newBuilder().setProject(projectId).build();
+    this.metricServiceClient = metricServiceClient;
+    this.viewManager = viewManager;
+    this.monitoredResource = monitoredResource;
+    this.domain = getDomain(metricNamePrefix);
+    this.displayNamePrefix = getDisplayNamePrefix(metricNamePrefix);
+
+    Tracing.getExportComponent()
+        .getSampledSpanStore()
+        .registerSpanNamesForCollection(
+            Collections.singletonList("ExportStatsToStackdriverMonitoring"));
+  }
+
+  // Returns true if the given view is successfully registered to Stackdriver Monitoring, or the
+  // exact same view has already been registered. Returns false otherwise.
+  @VisibleForTesting
+  boolean registerView(View view) {
+    View existing = registeredViews.get(view.getName());
+    if (existing != null) {
+      if (existing.equals(view)) {
+        // Ignore views that are already registered.
+        return true;
+      } else {
+        // If we upload a view that has the same name with a registered view but with different
+        // attributes, Stackdriver client will throw an exception.
+        logger.log(
+            Level.WARNING,
+            "A different view with the same name is already registered: " + existing);
+        return false;
+      }
+    }
+    registeredViews.put(view.getName(), view);
+
+    Span span = tracer.getCurrentSpan();
+    span.addAnnotation("Create Stackdriver Metric.");
+    // TODO(songya): don't need to create MetricDescriptor for RpcViewConstants once we defined
+    // canonical metrics. Registration is required only for custom view definitions. Canonical
+    // views should be pre-registered.
+    MetricDescriptor metricDescriptor =
+        StackdriverExportUtils.createMetricDescriptor(view, projectId, domain, displayNamePrefix);
+    if (metricDescriptor == null) {
+      // Don't register interval views in this version.
+      return false;
+    }
+
+    CreateMetricDescriptorRequest request =
+        CreateMetricDescriptorRequest.newBuilder()
+            .setName(projectName.toString())
+            .setMetricDescriptor(metricDescriptor)
+            .build();
+    try {
+      metricServiceClient.createMetricDescriptor(request);
+      span.addAnnotation("Finish creating MetricDescriptor.");
+      return true;
+    } catch (ApiException e) {
+      logger.log(Level.WARNING, "ApiException thrown when creating MetricDescriptor.", e);
+      span.setStatus(
+          Status.CanonicalCode.valueOf(e.getStatusCode().getCode().name())
+              .toStatus()
+              .withDescription(
+                  "ApiException thrown when creating MetricDescriptor: " + exceptionMessage(e)));
+      return false;
+    } catch (Throwable e) {
+      logger.log(Level.WARNING, "Exception thrown when creating MetricDescriptor.", e);
+      span.setStatus(
+          Status.UNKNOWN.withDescription(
+              "Exception thrown when creating MetricDescriptor: " + exceptionMessage(e)));
+      return false;
+    }
+  }
+
+  // Polls ViewData from Stats library for all exported views, and upload them as TimeSeries to
+  // StackDriver.
+  @VisibleForTesting
+  void export() {
+    List</*@Nullable*/ ViewData> viewDataList = Lists.newArrayList();
+    for (View view : viewManager.getAllExportedViews()) {
+      if (registerView(view)) {
+        // Only upload stats for valid views.
+        viewDataList.add(viewManager.getView(view.getName()));
+      }
+    }
+
+    List<TimeSeries> timeSeriesList = Lists.newArrayList();
+    for (/*@Nullable*/ ViewData viewData : viewDataList) {
+      timeSeriesList.addAll(
+          StackdriverExportUtils.createTimeSeriesList(viewData, monitoredResource, domain));
+    }
+    for (List<TimeSeries> batchedTimeSeries :
+        Lists.partition(timeSeriesList, MAX_BATCH_EXPORT_SIZE)) {
+      Span span = tracer.getCurrentSpan();
+      span.addAnnotation("Export Stackdriver TimeSeries.");
+      try {
+        CreateTimeSeriesRequest request =
+            CreateTimeSeriesRequest.newBuilder()
+                .setName(projectName.toString())
+                .addAllTimeSeries(batchedTimeSeries)
+                .build();
+        metricServiceClient.createTimeSeries(request);
+        span.addAnnotation("Finish exporting TimeSeries.");
+      } catch (ApiException e) {
+        logger.log(Level.WARNING, "ApiException thrown when exporting TimeSeries.", e);
+        span.setStatus(
+            Status.CanonicalCode.valueOf(e.getStatusCode().getCode().name())
+                .toStatus()
+                .withDescription(
+                    "ApiException thrown when exporting TimeSeries: " + exceptionMessage(e)));
+      } catch (Throwable e) {
+        logger.log(Level.WARNING, "Exception thrown when exporting TimeSeries.", e);
+        span.setStatus(
+            Status.UNKNOWN.withDescription(
+                "Exception thrown when exporting TimeSeries: " + exceptionMessage(e)));
+      }
+    }
+  }
+
+  @Override
+  public void run() {
+    while (true) {
+      Span span =
+          tracer
+              .spanBuilder("ExportStatsToStackdriverMonitoring")
+              .setRecordEvents(true)
+              .setSampler(probabilitySampler)
+              .startSpan();
+      Scope scope = tracer.withSpan(span);
+      try {
+        export();
+      } catch (Throwable e) {
+        logger.log(Level.WARNING, "Exception thrown by the Stackdriver stats exporter.", e);
+        span.setStatus(
+            Status.UNKNOWN.withDescription(
+                "Exception from Stackdriver Exporter: " + exceptionMessage(e)));
+      } finally {
+        scope.close();
+        span.end();
+      }
+      try {
+        Thread.sleep(scheduleDelayMillis);
+      } catch (InterruptedException ie) {
+        // Preserve the interruption status as per guidance and stop doing any work.
+        Thread.currentThread().interrupt();
+        return;
+      }
+    }
+  }
+
+  private static String exceptionMessage(Throwable e) {
+    return e.getMessage() != null ? e.getMessage() : e.getClass().getName();
+  }
+
+  @VisibleForTesting
+  static String getDomain(@javax.annotation.Nullable String metricNamePrefix) {
+    String domain;
+    if (Strings.isNullOrEmpty(metricNamePrefix)) {
+      domain = CUSTOM_OPENCENSUS_DOMAIN;
+    } else {
+      if (!metricNamePrefix.endsWith("/")) {
+        domain = metricNamePrefix + '/';
+      } else {
+        domain = metricNamePrefix;
+      }
+    }
+    return domain;
+  }
+
+  @VisibleForTesting
+  static String getDisplayNamePrefix(@javax.annotation.Nullable String metricNamePrefix) {
+    if (metricNamePrefix == null) {
+      return DEFAULT_DISPLAY_NAME_PREFIX;
+    } else {
+      if (!metricNamePrefix.endsWith("/") && !metricNamePrefix.isEmpty()) {
+        metricNamePrefix += '/';
+      }
+      return metricNamePrefix;
+    }
+  }
+}
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java
new file mode 100644
index 0000000..c4008ca
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java
@@ -0,0 +1,159 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import com.google.api.MonitoredResource;
+import com.google.auth.Credentials;
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link StackdriverStatsExporter}.
+ *
+ * @since 0.11
+ */
+@AutoValue
+@Immutable
+public abstract class StackdriverStatsConfiguration {
+
+  StackdriverStatsConfiguration() {}
+
+  /**
+   * Returns the {@link Credentials}.
+   *
+   * @return the {@code Credentials}.
+   * @since 0.11
+   */
+  @Nullable
+  public abstract Credentials getCredentials();
+
+  /**
+   * Returns the project id.
+   *
+   * @return the project id.
+   * @since 0.11
+   */
+  @Nullable
+  public abstract String getProjectId();
+
+  /**
+   * Returns the export interval between pushes to StackDriver.
+   *
+   * @return the export interval.
+   * @since 0.11
+   */
+  @Nullable
+  public abstract Duration getExportInterval();
+
+  /**
+   * Returns the Stackdriver {@link MonitoredResource}.
+   *
+   * @return the {@code MonitoredResource}.
+   * @since 0.11
+   */
+  @Nullable
+  public abstract MonitoredResource getMonitoredResource();
+
+  /**
+   * Returns the name prefix for Stackdriver metrics.
+   *
+   * @return the metric name prefix.
+   * @since 0.16
+   */
+  @Nullable
+  public abstract String getMetricNamePrefix();
+
+  /**
+   * Returns a new {@link Builder}.
+   *
+   * @return a {@code Builder}.
+   * @since 0.11
+   */
+  public static Builder builder() {
+    return new AutoValue_StackdriverStatsConfiguration.Builder();
+  }
+
+  /**
+   * Builder for {@link StackdriverStatsConfiguration}.
+   *
+   * @since 0.11
+   */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    Builder() {}
+
+    /**
+     * Sets the given {@link Credentials}.
+     *
+     * @param credentials the {@code Credentials}.
+     * @return this.
+     * @since 0.11
+     */
+    public abstract Builder setCredentials(Credentials credentials);
+
+    /**
+     * Sets the given project id.
+     *
+     * @param projectId the cloud project id.
+     * @return this.
+     * @since 0.11
+     */
+    public abstract Builder setProjectId(String projectId);
+
+    /**
+     * Sets the export interval.
+     *
+     * @param exportInterval the export interval between pushes to StackDriver.
+     * @return this.
+     * @since 0.11
+     */
+    public abstract Builder setExportInterval(Duration exportInterval);
+
+    /**
+     * Sets the {@link MonitoredResource}.
+     *
+     * @param monitoredResource the Stackdriver {@code MonitoredResource}.
+     * @return this.
+     * @since 0.11
+     */
+    public abstract Builder setMonitoredResource(MonitoredResource monitoredResource);
+
+    /**
+     * Sets the the name prefix for Stackdriver metrics.
+     *
+     * <p>It is suggested to use prefix with custom or external domain name, for example
+     * "custom.googleapis.com/myorg/" or "external.googleapis.com/prometheus/". If the given prefix
+     * doesn't start with a valid domain, we will add "custom.googleapis.com/" before the prefix.
+     *
+     * @param prefix the metric name prefix.
+     * @return this.
+     * @since 0.16
+     */
+    public abstract Builder setMetricNamePrefix(String prefix);
+
+    /**
+     * Builds a new {@link StackdriverStatsConfiguration} with current settings.
+     *
+     * @return a {@code StackdriverStatsConfiguration}.
+     * @since 0.11
+     */
+    public abstract StackdriverStatsConfiguration build();
+  }
+}
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java
new file mode 100644
index 0000000..51c5491
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java
@@ -0,0 +1,363 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.api.MonitoredResource;
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.cloud.monitoring.v3.MetricServiceSettings;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.MoreExecutors;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.ViewManager;
+import java.io.IOException;
+import java.util.concurrent.ThreadFactory;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Exporter to Stackdriver Monitoring Client API v3.
+ *
+ * <p>Example of usage on Google Cloud VMs:
+ *
+ * <pre><code>
+ *   public static void main(String[] args) {
+ *     StackdriverStatsExporter.createAndRegister(
+ *         StackdriverStatsConfiguration
+ *             .builder()
+ *             .setProjectId("MyStackdriverProjectId")
+ *             .setExportInterval(Duration.fromMillis(100000))
+ *             .build());
+ *     ... // Do work.
+ *   }
+ * </code></pre>
+ *
+ * @since 0.9
+ */
+public final class StackdriverStatsExporter {
+
+  private static final Object monitor = new Object();
+
+  private final Thread workerThread;
+
+  @GuardedBy("monitor")
+  @Nullable
+  private static StackdriverStatsExporter exporter = null;
+
+  private static final Duration ZERO = Duration.create(0, 0);
+
+  @VisibleForTesting static final Duration DEFAULT_INTERVAL = Duration.create(60, 0);
+
+  private static final MonitoredResource DEFAULT_RESOURCE =
+      StackdriverExportUtils.getDefaultResource();
+
+  @VisibleForTesting
+  StackdriverStatsExporter(
+      String projectId,
+      MetricServiceClient metricServiceClient,
+      Duration exportInterval,
+      ViewManager viewManager,
+      MonitoredResource monitoredResource,
+      @Nullable String metricNamePrefix) {
+    checkArgument(exportInterval.compareTo(ZERO) > 0, "Duration must be positive");
+    StackdriverExporterWorker worker =
+        new StackdriverExporterWorker(
+            projectId,
+            metricServiceClient,
+            exportInterval,
+            viewManager,
+            monitoredResource,
+            metricNamePrefix);
+    this.workerThread = new DaemonThreadFactory().newThread(worker);
+  }
+
+  /**
+   * Creates a StackdriverStatsExporter for an explicit project ID and using explicit credentials,
+   * with default Monitored Resource.
+   *
+   * <p>Only one Stackdriver exporter can be created.
+   *
+   * @param credentials a credentials used to authenticate API calls.
+   * @param projectId the cloud project id.
+   * @param exportInterval the interval between pushing stats to StackDriver.
+   * @throws IllegalStateException if a Stackdriver exporter already exists.
+   * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+   * @since 0.9
+   */
+  @Deprecated
+  public static void createAndRegisterWithCredentialsAndProjectId(
+      Credentials credentials, String projectId, Duration exportInterval) throws IOException {
+    checkNotNull(credentials, "credentials");
+    checkNotNull(projectId, "projectId");
+    checkNotNull(exportInterval, "exportInterval");
+    createInternal(credentials, projectId, exportInterval, null, null);
+  }
+
+  /**
+   * Creates a Stackdriver Stats exporter for an explicit project ID, with default Monitored
+   * Resource.
+   *
+   * <p>Only one Stackdriver exporter can be created.
+   *
+   * <p>This uses the default application credentials. See {@link
+   * GoogleCredentials#getApplicationDefault}.
+   *
+   * <p>This is equivalent with:
+   *
+   * <pre>{@code
+   * StackdriverStatsExporter.createWithCredentialsAndProjectId(
+   *     GoogleCredentials.getApplicationDefault(), projectId);
+   * }</pre>
+   *
+   * @param projectId the cloud project id.
+   * @param exportInterval the interval between pushing stats to StackDriver.
+   * @throws IllegalStateException if a Stackdriver exporter is already created.
+   * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+   * @since 0.9
+   */
+  @Deprecated
+  public static void createAndRegisterWithProjectId(String projectId, Duration exportInterval)
+      throws IOException {
+    checkNotNull(projectId, "projectId");
+    checkNotNull(exportInterval, "exportInterval");
+    createInternal(null, projectId, exportInterval, null, null);
+  }
+
+  /**
+   * Creates a Stackdriver Stats exporter with a {@link StackdriverStatsConfiguration}.
+   *
+   * <p>Only one Stackdriver exporter can be created.
+   *
+   * <p>If {@code credentials} of the configuration is not set, the exporter will use the default
+   * application credentials. See {@link GoogleCredentials#getApplicationDefault}.
+   *
+   * <p>If {@code projectId} of the configuration is not set, the exporter will use the default
+   * project ID configured. See {@link ServiceOptions#getDefaultProjectId}.
+   *
+   * <p>If {@code exportInterval} of the configuration is not set, the exporter will use the default
+   * interval of one minute.
+   *
+   * <p>If {@code monitoredResources} of the configuration is not set, the exporter will try to
+   * create an appropriate {@code monitoredResources} based on the environment variables. In
+   * addition, please refer to
+   * cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource for a list of valid
+   * {@code MonitoredResource}s.
+   *
+   * <p>If {@code metricNamePrefix} of the configuration is not set, the exporter will use the
+   * default prefix "OpenCensus".
+   *
+   * @param configuration the {@code StackdriverStatsConfiguration}.
+   * @throws IllegalStateException if a Stackdriver exporter is already created.
+   * @since 0.11.0
+   */
+  public static void createAndRegister(StackdriverStatsConfiguration configuration)
+      throws IOException {
+    checkNotNull(configuration, "configuration");
+    createInternal(
+        configuration.getCredentials(),
+        configuration.getProjectId(),
+        configuration.getExportInterval(),
+        configuration.getMonitoredResource(),
+        configuration.getMetricNamePrefix());
+  }
+
+  /**
+   * Creates a Stackdriver Stats exporter with default settings.
+   *
+   * <p>Only one Stackdriver exporter can be created.
+   *
+   * <p>This is equivalent with:
+   *
+   * <pre>{@code
+   * StackdriverStatsExporter.createAndRegister(StackdriverStatsConfiguration.builder().build());
+   * }</pre>
+   *
+   * <p>This method uses the default application credentials. See {@link
+   * GoogleCredentials#getApplicationDefault}.
+   *
+   * <p>This method uses the default project ID configured. See {@link
+   * ServiceOptions#getDefaultProjectId}.
+   *
+   * <p>This method uses the default interval of one minute.
+   *
+   * <p>This method uses the default resource created from the environment variables.
+   *
+   * <p>This method uses the default display name prefix "OpenCensus".
+   *
+   * @throws IllegalStateException if a Stackdriver exporter is already created.
+   * @since 0.11.0
+   */
+  public static void createAndRegister() throws IOException {
+    createInternal(null, null, null, null, null);
+  }
+
+  /**
+   * Creates a Stackdriver Stats exporter with default Monitored Resource.
+   *
+   * <p>Only one Stackdriver exporter can be created.
+   *
+   * <p>This uses the default application credentials. See {@link
+   * GoogleCredentials#getApplicationDefault}.
+   *
+   * <p>This uses the default project ID configured see {@link ServiceOptions#getDefaultProjectId}.
+   *
+   * <p>This is equivalent with:
+   *
+   * <pre>{@code
+   * StackdriverStatsExporter.createWithProjectId(ServiceOptions.getDefaultProjectId());
+   * }</pre>
+   *
+   * @param exportInterval the interval between pushing stats to StackDriver.
+   * @throws IllegalStateException if a Stackdriver exporter is already created.
+   * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+   * @since 0.9
+   */
+  @Deprecated
+  public static void createAndRegister(Duration exportInterval) throws IOException {
+    checkNotNull(exportInterval, "exportInterval");
+    createInternal(null, null, exportInterval, null, null);
+  }
+
+  /**
+   * Creates a Stackdriver Stats exporter with an explicit project ID and a custom Monitored
+   * Resource.
+   *
+   * <p>Only one Stackdriver exporter can be created.
+   *
+   * <p>Please refer to cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource
+   * for a list of valid {@code MonitoredResource}s.
+   *
+   * <p>This uses the default application credentials. See {@link
+   * GoogleCredentials#getApplicationDefault}.
+   *
+   * @param projectId the cloud project id.
+   * @param exportInterval the interval between pushing stats to StackDriver.
+   * @param monitoredResource the Monitored Resource used by exporter.
+   * @throws IllegalStateException if a Stackdriver exporter is already created.
+   * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+   * @since 0.10
+   */
+  @Deprecated
+  public static void createAndRegisterWithProjectIdAndMonitoredResource(
+      String projectId, Duration exportInterval, MonitoredResource monitoredResource)
+      throws IOException {
+    checkNotNull(projectId, "projectId");
+    checkNotNull(exportInterval, "exportInterval");
+    checkNotNull(monitoredResource, "monitoredResource");
+    createInternal(null, projectId, exportInterval, monitoredResource, null);
+  }
+
+  /**
+   * Creates a Stackdriver Stats exporter with a custom Monitored Resource.
+   *
+   * <p>Only one Stackdriver exporter can be created.
+   *
+   * <p>Please refer to cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource
+   * for a list of valid {@code MonitoredResource}s.
+   *
+   * <p>This uses the default application credentials. See {@link
+   * GoogleCredentials#getApplicationDefault}.
+   *
+   * <p>This uses the default project ID configured see {@link ServiceOptions#getDefaultProjectId}.
+   *
+   * @param exportInterval the interval between pushing stats to StackDriver.
+   * @param monitoredResource the Monitored Resource used by exporter.
+   * @throws IllegalStateException if a Stackdriver exporter is already created.
+   * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+   * @since 0.10
+   */
+  @Deprecated
+  public static void createAndRegisterWithMonitoredResource(
+      Duration exportInterval, MonitoredResource monitoredResource) throws IOException {
+    checkNotNull(exportInterval, "exportInterval");
+    checkNotNull(monitoredResource, "monitoredResource");
+    createInternal(null, null, exportInterval, monitoredResource, null);
+  }
+
+  // Use createInternal() (instead of constructor) to enforce singleton.
+  private static void createInternal(
+      @Nullable Credentials credentials,
+      @Nullable String projectId,
+      @Nullable Duration exportInterval,
+      @Nullable MonitoredResource monitoredResource,
+      @Nullable String metricNamePrefix)
+      throws IOException {
+    projectId = projectId == null ? ServiceOptions.getDefaultProjectId() : projectId;
+    exportInterval = exportInterval == null ? DEFAULT_INTERVAL : exportInterval;
+    monitoredResource = monitoredResource == null ? DEFAULT_RESOURCE : monitoredResource;
+    synchronized (monitor) {
+      checkState(exporter == null, "Stackdriver stats exporter is already created.");
+      MetricServiceClient metricServiceClient;
+      // Initialize MetricServiceClient inside lock to avoid creating multiple clients.
+      if (credentials == null) {
+        metricServiceClient = MetricServiceClient.create();
+      } else {
+        metricServiceClient =
+            MetricServiceClient.create(
+                MetricServiceSettings.newBuilder()
+                    .setCredentialsProvider(FixedCredentialsProvider.create(credentials))
+                    .build());
+      }
+      exporter =
+          new StackdriverStatsExporter(
+              projectId,
+              metricServiceClient,
+              exportInterval,
+              Stats.getViewManager(),
+              monitoredResource,
+              metricNamePrefix);
+      exporter.workerThread.start();
+    }
+  }
+
+  // Resets exporter to null. Used only for unit tests.
+  @VisibleForTesting
+  static void unsafeResetExporter() {
+    synchronized (monitor) {
+      StackdriverStatsExporter.exporter = null;
+    }
+  }
+
+  /** A lightweight {@link ThreadFactory} to spawn threads in a GAE-Java7-compatible way. */
+  // TODO(Hailong): Remove this once we use a callback to implement the exporter.
+  static final class DaemonThreadFactory implements ThreadFactory {
+    // AppEngine runtimes have constraints on threading and socket handling
+    // that need to be accommodated.
+    public static final boolean IS_RESTRICTED_APPENGINE =
+        System.getProperty("com.google.appengine.runtime.environment") != null
+            && "1.7".equals(System.getProperty("java.specification.version"));
+    private static final ThreadFactory threadFactory = MoreExecutors.platformThreadFactory();
+
+    @Override
+    public Thread newThread(Runnable r) {
+      Thread thread = threadFactory.newThread(r);
+      if (!IS_RESTRICTED_APPENGINE) {
+        thread.setName("ExportWorkerThread");
+        thread.setDaemon(true);
+      }
+      return thread;
+    }
+  }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java
new file mode 100644
index 0000000..cd536e8
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java
@@ -0,0 +1,568 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.CUSTOM_OPENCENSUS_DOMAIN;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.DEFAULT_DISPLAY_NAME_PREFIX;
+
+import com.google.api.Distribution.BucketOptions;
+import com.google.api.Distribution.BucketOptions.Explicit;
+import com.google.api.LabelDescriptor;
+import com.google.api.LabelDescriptor.ValueType;
+import com.google.api.Metric;
+import com.google.api.MetricDescriptor;
+import com.google.api.MetricDescriptor.MetricKind;
+import com.google.api.MonitoredResource;
+import com.google.common.collect.ImmutableMap;
+import com.google.monitoring.v3.Point;
+import com.google.monitoring.v3.TimeInterval;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.monitoring.v3.TypedValue;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.lang.management.ManagementFactory;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverExportUtils}. */
+@RunWith(JUnit4.class)
+public class StackdriverExportUtilsTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private static final TagKey KEY = TagKey.create("KEY");
+  private static final TagKey KEY_2 = TagKey.create("KEY2");
+  private static final TagKey KEY_3 = TagKey.create("KEY3");
+  private static final TagValue VALUE_1 = TagValue.create("VALUE1");
+  private static final TagValue VALUE_2 = TagValue.create("VALUE2");
+  private static final String MEASURE_UNIT = "us";
+  private static final String MEASURE_DESCRIPTION = "measure description";
+  private static final MeasureDouble MEASURE_DOUBLE =
+      MeasureDouble.create("measure1", MEASURE_DESCRIPTION, MEASURE_UNIT);
+  private static final MeasureLong MEASURE_LONG =
+      MeasureLong.create("measure2", MEASURE_DESCRIPTION, MEASURE_UNIT);
+  private static final String VIEW_NAME = "view";
+  private static final String VIEW_DESCRIPTION = "view description";
+  private static final Duration TEN_SECONDS = Duration.create(10, 0);
+  private static final Cumulative CUMULATIVE = Cumulative.create();
+  private static final Interval INTERVAL = Interval.create(TEN_SECONDS);
+  private static final BucketBoundaries BUCKET_BOUNDARIES =
+      BucketBoundaries.create(Arrays.asList(0.0, 1.0, 3.0, 5.0));
+  private static final Sum SUM = Sum.create();
+  private static final Count COUNT = Count.create();
+  private static final Mean MEAN = Mean.create();
+  private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+  private static final LastValue LAST_VALUE = LastValue.create();
+  private static final String PROJECT_ID = "id";
+  private static final MonitoredResource DEFAULT_RESOURCE =
+      MonitoredResource.newBuilder().setType("global").build();
+  private static final String DEFAULT_TASK_VALUE =
+      "java-" + ManagementFactory.getRuntimeMXBean().getName();
+
+  @Test
+  public void testConstant() {
+    assertThat(StackdriverExportUtils.LABEL_DESCRIPTION).isEqualTo("OpenCensus TagKey");
+  }
+
+  @Test
+  public void createLabelDescriptor() {
+    assertThat(StackdriverExportUtils.createLabelDescriptor(TagKey.create("string")))
+        .isEqualTo(
+            LabelDescriptor.newBuilder()
+                .setKey("string")
+                .setDescription(StackdriverExportUtils.LABEL_DESCRIPTION)
+                .setValueType(ValueType.STRING)
+                .build());
+  }
+
+  @Test
+  public void createMetricKind() {
+    assertThat(StackdriverExportUtils.createMetricKind(CUMULATIVE, SUM))
+        .isEqualTo(MetricKind.CUMULATIVE);
+    assertThat(StackdriverExportUtils.createMetricKind(INTERVAL, COUNT))
+        .isEqualTo(MetricKind.UNRECOGNIZED);
+    assertThat(StackdriverExportUtils.createMetricKind(CUMULATIVE, LAST_VALUE))
+        .isEqualTo(MetricKind.GAUGE);
+    assertThat(StackdriverExportUtils.createMetricKind(INTERVAL, LAST_VALUE))
+        .isEqualTo(MetricKind.GAUGE);
+  }
+
+  @Test
+  public void createValueType() {
+    assertThat(StackdriverExportUtils.createValueType(SUM, MEASURE_DOUBLE))
+        .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+    assertThat(StackdriverExportUtils.createValueType(SUM, MEASURE_LONG))
+        .isEqualTo(MetricDescriptor.ValueType.INT64);
+    assertThat(StackdriverExportUtils.createValueType(COUNT, MEASURE_DOUBLE))
+        .isEqualTo(MetricDescriptor.ValueType.INT64);
+    assertThat(StackdriverExportUtils.createValueType(COUNT, MEASURE_LONG))
+        .isEqualTo(MetricDescriptor.ValueType.INT64);
+    assertThat(StackdriverExportUtils.createValueType(MEAN, MEASURE_DOUBLE))
+        .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+    assertThat(StackdriverExportUtils.createValueType(MEAN, MEASURE_LONG))
+        .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+    assertThat(StackdriverExportUtils.createValueType(DISTRIBUTION, MEASURE_DOUBLE))
+        .isEqualTo(MetricDescriptor.ValueType.DISTRIBUTION);
+    assertThat(StackdriverExportUtils.createValueType(DISTRIBUTION, MEASURE_LONG))
+        .isEqualTo(MetricDescriptor.ValueType.DISTRIBUTION);
+    assertThat(StackdriverExportUtils.createValueType(LAST_VALUE, MEASURE_DOUBLE))
+        .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+    assertThat(StackdriverExportUtils.createValueType(LAST_VALUE, MEASURE_LONG))
+        .isEqualTo(MetricDescriptor.ValueType.INT64);
+  }
+
+  @Test
+  public void createUnit() {
+    assertThat(StackdriverExportUtils.createUnit(SUM, MEASURE_DOUBLE)).isEqualTo(MEASURE_UNIT);
+    assertThat(StackdriverExportUtils.createUnit(COUNT, MEASURE_DOUBLE)).isEqualTo("1");
+    assertThat(StackdriverExportUtils.createUnit(MEAN, MEASURE_DOUBLE)).isEqualTo(MEASURE_UNIT);
+    assertThat(StackdriverExportUtils.createUnit(DISTRIBUTION, MEASURE_DOUBLE))
+        .isEqualTo(MEASURE_UNIT);
+    assertThat(StackdriverExportUtils.createUnit(LAST_VALUE, MEASURE_DOUBLE))
+        .isEqualTo(MEASURE_UNIT);
+  }
+
+  @Test
+  public void createMetric() {
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    assertThat(
+            StackdriverExportUtils.createMetric(
+                view, Arrays.asList(VALUE_1), CUSTOM_OPENCENSUS_DOMAIN))
+        .isEqualTo(
+            Metric.newBuilder()
+                .setType("custom.googleapis.com/opencensus/" + VIEW_NAME)
+                .putLabels("KEY", "VALUE1")
+                .putLabels(StackdriverExportUtils.OPENCENSUS_TASK, DEFAULT_TASK_VALUE)
+                .build());
+  }
+
+  @Test
+  public void createMetric_WithExternalMetricDomain() {
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    String prometheusDomain = "external.googleapis.com/prometheus/";
+    assertThat(StackdriverExportUtils.createMetric(view, Arrays.asList(VALUE_1), prometheusDomain))
+        .isEqualTo(
+            Metric.newBuilder()
+                .setType(prometheusDomain + VIEW_NAME)
+                .putLabels("KEY", "VALUE1")
+                .putLabels(StackdriverExportUtils.OPENCENSUS_TASK, DEFAULT_TASK_VALUE)
+                .build());
+  }
+
+  @Test
+  public void createMetric_skipNullTagValue() {
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY, KEY_2, KEY_3),
+            CUMULATIVE);
+    assertThat(
+            StackdriverExportUtils.createMetric(
+                view, Arrays.asList(VALUE_1, null, VALUE_2), CUSTOM_OPENCENSUS_DOMAIN))
+        .isEqualTo(
+            Metric.newBuilder()
+                .setType("custom.googleapis.com/opencensus/" + VIEW_NAME)
+                .putLabels("KEY", "VALUE1")
+                .putLabels("KEY3", "VALUE2")
+                .putLabels(StackdriverExportUtils.OPENCENSUS_TASK, DEFAULT_TASK_VALUE)
+                .build());
+  }
+
+  @Test
+  public void createMetric_throwWhenTagKeysAndValuesHaveDifferentSize() {
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY, KEY_2, KEY_3),
+            CUMULATIVE);
+    List<TagValue> tagValues = Arrays.asList(VALUE_1, null);
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("TagKeys and TagValues don't have same size.");
+    StackdriverExportUtils.createMetric(view, tagValues, CUSTOM_OPENCENSUS_DOMAIN);
+  }
+
+  @Test
+  public void convertTimestamp() {
+    Timestamp censusTimestamp1 = Timestamp.create(100, 3000);
+    assertThat(StackdriverExportUtils.convertTimestamp(censusTimestamp1))
+        .isEqualTo(
+            com.google.protobuf.Timestamp.newBuilder().setSeconds(100).setNanos(3000).build());
+
+    // Stackdriver doesn't allow negative values, instead it will replace the negative values
+    // by returning a default instance.
+    Timestamp censusTimestamp2 = Timestamp.create(-100, 3000);
+    assertThat(StackdriverExportUtils.convertTimestamp(censusTimestamp2))
+        .isEqualTo(com.google.protobuf.Timestamp.newBuilder().build());
+  }
+
+  @Test
+  public void createTimeInterval_cumulative() {
+    Timestamp censusTimestamp1 = Timestamp.create(100, 3000);
+    Timestamp censusTimestamp2 = Timestamp.create(200, 0);
+    assertThat(
+            StackdriverExportUtils.createTimeInterval(
+                CumulativeData.create(censusTimestamp1, censusTimestamp2), DISTRIBUTION))
+        .isEqualTo(
+            TimeInterval.newBuilder()
+                .setStartTime(StackdriverExportUtils.convertTimestamp(censusTimestamp1))
+                .setEndTime(StackdriverExportUtils.convertTimestamp(censusTimestamp2))
+                .build());
+    assertThat(
+            StackdriverExportUtils.createTimeInterval(
+                CumulativeData.create(censusTimestamp1, censusTimestamp2), LAST_VALUE))
+        .isEqualTo(
+            TimeInterval.newBuilder()
+                .setEndTime(StackdriverExportUtils.convertTimestamp(censusTimestamp2))
+                .build());
+  }
+
+  @Test
+  public void createTimeInterval_interval() {
+    IntervalData intervalData = IntervalData.create(Timestamp.create(200, 0));
+    // Only Cumulative view will supported in this version.
+    thrown.expect(IllegalArgumentException.class);
+    StackdriverExportUtils.createTimeInterval(intervalData, SUM);
+  }
+
+  @Test
+  public void createBucketOptions() {
+    assertThat(StackdriverExportUtils.createBucketOptions(BUCKET_BOUNDARIES))
+        .isEqualTo(
+            BucketOptions.newBuilder()
+                .setExplicitBuckets(
+                    Explicit.newBuilder().addAllBounds(Arrays.asList(0.0, 1.0, 3.0, 5.0)))
+                .build());
+  }
+
+  @Test
+  public void createDistribution() {
+    DistributionData distributionData =
+        DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L));
+    assertThat(StackdriverExportUtils.createDistribution(distributionData, BUCKET_BOUNDARIES))
+        .isEqualTo(
+            com.google.api.Distribution.newBuilder()
+                .setMean(2)
+                .setCount(3)
+                // TODO(songya): uncomment this once Stackdriver supports setting max and min.
+                // .setRange(
+                //     com.google.api.Distribution.Range.newBuilder().setMin(0).setMax(5).build())
+                .setBucketOptions(StackdriverExportUtils.createBucketOptions(BUCKET_BOUNDARIES))
+                .addAllBucketCounts(Arrays.asList(0L, 1L, 1L, 0L, 1L))
+                .setSumOfSquaredDeviation(14)
+                .build());
+  }
+
+  @Test
+  public void createTypedValue() {
+    assertThat(StackdriverExportUtils.createTypedValue(SUM, SumDataDouble.create(1.1)))
+        .isEqualTo(TypedValue.newBuilder().setDoubleValue(1.1).build());
+    assertThat(StackdriverExportUtils.createTypedValue(SUM, SumDataLong.create(10000)))
+        .isEqualTo(TypedValue.newBuilder().setInt64Value(10000).build());
+    assertThat(StackdriverExportUtils.createTypedValue(COUNT, CountData.create(55)))
+        .isEqualTo(TypedValue.newBuilder().setInt64Value(55).build());
+    assertThat(StackdriverExportUtils.createTypedValue(MEAN, MeanData.create(7.7, 8)))
+        .isEqualTo(TypedValue.newBuilder().setDoubleValue(7.7).build());
+    DistributionData distributionData =
+        DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L));
+    assertThat(StackdriverExportUtils.createTypedValue(DISTRIBUTION, distributionData))
+        .isEqualTo(
+            TypedValue.newBuilder()
+                .setDistributionValue(
+                    StackdriverExportUtils.createDistribution(distributionData, BUCKET_BOUNDARIES))
+                .build());
+    assertThat(StackdriverExportUtils.createTypedValue(LAST_VALUE, LastValueDataDouble.create(9.9)))
+        .isEqualTo(TypedValue.newBuilder().setDoubleValue(9.9).build());
+    assertThat(StackdriverExportUtils.createTypedValue(LAST_VALUE, LastValueDataLong.create(90000)))
+        .isEqualTo(TypedValue.newBuilder().setInt64Value(90000).build());
+  }
+
+  @Test
+  public void createPoint_cumulative() {
+    Timestamp censusTimestamp1 = Timestamp.create(100, 3000);
+    Timestamp censusTimestamp2 = Timestamp.create(200, 0);
+    CumulativeData cumulativeData = CumulativeData.create(censusTimestamp1, censusTimestamp2);
+    SumDataDouble sumDataDouble = SumDataDouble.create(33.3);
+
+    assertThat(StackdriverExportUtils.createPoint(sumDataDouble, cumulativeData, SUM))
+        .isEqualTo(
+            Point.newBuilder()
+                .setInterval(StackdriverExportUtils.createTimeInterval(cumulativeData, SUM))
+                .setValue(StackdriverExportUtils.createTypedValue(SUM, sumDataDouble))
+                .build());
+  }
+
+  @Test
+  public void createPoint_interval() {
+    IntervalData intervalData = IntervalData.create(Timestamp.create(200, 0));
+    SumDataDouble sumDataDouble = SumDataDouble.create(33.3);
+    // Only Cumulative view will supported in this version.
+    thrown.expect(IllegalArgumentException.class);
+    StackdriverExportUtils.createPoint(sumDataDouble, intervalData, SUM);
+  }
+
+  @Test
+  public void createMetricDescriptor_cumulative() {
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    MetricDescriptor metricDescriptor =
+        StackdriverExportUtils.createMetricDescriptor(
+            view, PROJECT_ID, "custom.googleapis.com/myorg/", "myorg/");
+    assertThat(metricDescriptor.getName())
+        .isEqualTo(
+            "projects/"
+                + PROJECT_ID
+                + "/metricDescriptors/custom.googleapis.com/myorg/"
+                + VIEW_NAME);
+    assertThat(metricDescriptor.getDescription()).isEqualTo(VIEW_DESCRIPTION);
+    assertThat(metricDescriptor.getDisplayName()).isEqualTo("myorg/" + VIEW_NAME);
+    assertThat(metricDescriptor.getType()).isEqualTo("custom.googleapis.com/myorg/" + VIEW_NAME);
+    assertThat(metricDescriptor.getUnit()).isEqualTo(MEASURE_UNIT);
+    assertThat(metricDescriptor.getMetricKind()).isEqualTo(MetricKind.CUMULATIVE);
+    assertThat(metricDescriptor.getValueType()).isEqualTo(MetricDescriptor.ValueType.DISTRIBUTION);
+    assertThat(metricDescriptor.getLabelsList())
+        .containsExactly(
+            LabelDescriptor.newBuilder()
+                .setKey(KEY.getName())
+                .setDescription(StackdriverExportUtils.LABEL_DESCRIPTION)
+                .setValueType(ValueType.STRING)
+                .build(),
+            LabelDescriptor.newBuilder()
+                .setKey(StackdriverExportUtils.OPENCENSUS_TASK)
+                .setDescription(StackdriverExportUtils.OPENCENSUS_TASK_DESCRIPTION)
+                .setValueType(ValueType.STRING)
+                .build());
+  }
+
+  @Test
+  public void createMetricDescriptor_cumulative_count() {
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            COUNT,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    MetricDescriptor metricDescriptor =
+        StackdriverExportUtils.createMetricDescriptor(
+            view, PROJECT_ID, CUSTOM_OPENCENSUS_DOMAIN, DEFAULT_DISPLAY_NAME_PREFIX);
+    assertThat(metricDescriptor.getName())
+        .isEqualTo(
+            "projects/"
+                + PROJECT_ID
+                + "/metricDescriptors/custom.googleapis.com/opencensus/"
+                + VIEW_NAME);
+    assertThat(metricDescriptor.getDescription()).isEqualTo(VIEW_DESCRIPTION);
+    assertThat(metricDescriptor.getDisplayName()).isEqualTo("OpenCensus/" + VIEW_NAME);
+    assertThat(metricDescriptor.getType())
+        .isEqualTo("custom.googleapis.com/opencensus/" + VIEW_NAME);
+    assertThat(metricDescriptor.getUnit()).isEqualTo("1");
+    assertThat(metricDescriptor.getMetricKind()).isEqualTo(MetricKind.CUMULATIVE);
+    assertThat(metricDescriptor.getValueType()).isEqualTo(MetricDescriptor.ValueType.INT64);
+    assertThat(metricDescriptor.getLabelsList())
+        .containsExactly(
+            LabelDescriptor.newBuilder()
+                .setKey(KEY.getName())
+                .setDescription(StackdriverExportUtils.LABEL_DESCRIPTION)
+                .setValueType(ValueType.STRING)
+                .build(),
+            LabelDescriptor.newBuilder()
+                .setKey(StackdriverExportUtils.OPENCENSUS_TASK)
+                .setDescription(StackdriverExportUtils.OPENCENSUS_TASK_DESCRIPTION)
+                .setValueType(ValueType.STRING)
+                .build());
+  }
+
+  @Test
+  public void createMetricDescriptor_interval() {
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            INTERVAL);
+    assertThat(
+            StackdriverExportUtils.createMetricDescriptor(
+                view, PROJECT_ID, CUSTOM_OPENCENSUS_DOMAIN, DEFAULT_DISPLAY_NAME_PREFIX))
+        .isNull();
+  }
+
+  @Test
+  public void createTimeSeriesList_cumulative() {
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    DistributionData distributionData1 =
+        DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L));
+    DistributionData distributionData2 =
+        DistributionData.create(-1, 1, -1, -1, 0, Arrays.asList(1L, 0L, 0L, 0L, 0L));
+    Map<List<TagValue>, DistributionData> aggregationMap =
+        ImmutableMap.of(
+            Arrays.asList(VALUE_1), distributionData1, Arrays.asList(VALUE_2), distributionData2);
+    CumulativeData cumulativeData =
+        CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+    ViewData viewData = ViewData.create(view, aggregationMap, cumulativeData);
+    List<TimeSeries> timeSeriesList =
+        StackdriverExportUtils.createTimeSeriesList(
+            viewData, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN);
+    assertThat(timeSeriesList).hasSize(2);
+    TimeSeries expected1 =
+        TimeSeries.newBuilder()
+            .setMetricKind(MetricKind.CUMULATIVE)
+            .setValueType(MetricDescriptor.ValueType.DISTRIBUTION)
+            .setMetric(
+                StackdriverExportUtils.createMetric(
+                    view, Arrays.asList(VALUE_1), CUSTOM_OPENCENSUS_DOMAIN))
+            .setResource(MonitoredResource.newBuilder().setType("global"))
+            .addPoints(
+                StackdriverExportUtils.createPoint(distributionData1, cumulativeData, DISTRIBUTION))
+            .build();
+    TimeSeries expected2 =
+        TimeSeries.newBuilder()
+            .setMetricKind(MetricKind.CUMULATIVE)
+            .setValueType(MetricDescriptor.ValueType.DISTRIBUTION)
+            .setMetric(
+                StackdriverExportUtils.createMetric(
+                    view, Arrays.asList(VALUE_2), CUSTOM_OPENCENSUS_DOMAIN))
+            .setResource(MonitoredResource.newBuilder().setType("global"))
+            .addPoints(
+                StackdriverExportUtils.createPoint(distributionData2, cumulativeData, DISTRIBUTION))
+            .build();
+    assertThat(timeSeriesList).containsExactly(expected1, expected2);
+  }
+
+  @Test
+  public void createTimeSeriesList_interval() {
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            INTERVAL);
+    Map<List<TagValue>, DistributionData> aggregationMap =
+        ImmutableMap.of(
+            Arrays.asList(VALUE_1),
+            DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L)),
+            Arrays.asList(VALUE_2),
+            DistributionData.create(-1, 1, -1, -1, 0, Arrays.asList(1L, 0L, 0L, 0L, 0L)));
+    ViewData viewData =
+        ViewData.create(view, aggregationMap, IntervalData.create(Timestamp.fromMillis(2000)));
+    assertThat(
+            StackdriverExportUtils.createTimeSeriesList(
+                viewData, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN))
+        .isEmpty();
+  }
+
+  @Test
+  public void createTimeSeriesList_withCustomMonitoredResource() {
+    MonitoredResource resource =
+        MonitoredResource.newBuilder().setType("global").putLabels("key", "value").build();
+    View view =
+        View.create(
+            Name.create(VIEW_NAME),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            SUM,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    SumDataDouble sumData = SumDataDouble.create(55.5);
+    Map<List<TagValue>, SumDataDouble> aggregationMap =
+        ImmutableMap.of(Arrays.asList(VALUE_1), sumData);
+    CumulativeData cumulativeData =
+        CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+    ViewData viewData = ViewData.create(view, aggregationMap, cumulativeData);
+    List<TimeSeries> timeSeriesList =
+        StackdriverExportUtils.createTimeSeriesList(viewData, resource, CUSTOM_OPENCENSUS_DOMAIN);
+    assertThat(timeSeriesList)
+        .containsExactly(
+            TimeSeries.newBuilder()
+                .setMetricKind(MetricKind.CUMULATIVE)
+                .setValueType(MetricDescriptor.ValueType.DOUBLE)
+                .setMetric(
+                    StackdriverExportUtils.createMetric(
+                        view, Arrays.asList(VALUE_1), CUSTOM_OPENCENSUS_DOMAIN))
+                .setResource(resource)
+                .addPoints(StackdriverExportUtils.createPoint(sumData, cumulativeData, SUM))
+                .build());
+  }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java
new file mode 100644
index 0000000..2759382
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java
@@ -0,0 +1,310 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.CUSTOM_OPENCENSUS_DOMAIN;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.DEFAULT_DISPLAY_NAME_PREFIX;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.api.MetricDescriptor;
+import com.google.api.MonitoredResource;
+import com.google.api.gax.rpc.UnaryCallable;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.cloud.monitoring.v3.stub.MetricServiceStub;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.monitoring.v3.CreateMetricDescriptorRequest;
+import com.google.monitoring.v3.CreateTimeSeriesRequest;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.protobuf.Empty;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link StackdriverExporterWorker}. */
+@RunWith(JUnit4.class)
+public class StackdriverExporterWorkerTest {
+
+  private static final String PROJECT_ID = "projectId";
+  private static final Duration ONE_SECOND = Duration.create(1, 0);
+  private static final TagKey KEY = TagKey.create("KEY");
+  private static final TagValue VALUE = TagValue.create("VALUE");
+  private static final String MEASURE_NAME = "my measurement";
+  private static final String MEASURE_UNIT = "us";
+  private static final String MEASURE_DESCRIPTION = "measure description";
+  private static final MeasureLong MEASURE =
+      MeasureLong.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT);
+  private static final Name VIEW_NAME = Name.create("my view");
+  private static final String VIEW_DESCRIPTION = "view description";
+  private static final Cumulative CUMULATIVE = Cumulative.create();
+  private static final Interval INTERVAL = Interval.create(ONE_SECOND);
+  private static final Sum SUM = Sum.create();
+  private static final MonitoredResource DEFAULT_RESOURCE =
+      MonitoredResource.newBuilder().setType("global").build();
+
+  @Mock private ViewManager mockViewManager;
+
+  @Mock private MetricServiceStub mockStub;
+
+  @Mock
+  private UnaryCallable<CreateMetricDescriptorRequest, MetricDescriptor>
+      mockCreateMetricDescriptorCallable;
+
+  @Mock private UnaryCallable<CreateTimeSeriesRequest, Empty> mockCreateTimeSeriesCallable;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+
+    doReturn(mockCreateMetricDescriptorCallable).when(mockStub).createMetricDescriptorCallable();
+    doReturn(mockCreateTimeSeriesCallable).when(mockStub).createTimeSeriesCallable();
+    doReturn(null)
+        .when(mockCreateMetricDescriptorCallable)
+        .call(any(CreateMetricDescriptorRequest.class));
+    doReturn(null).when(mockCreateTimeSeriesCallable).call(any(CreateTimeSeriesRequest.class));
+  }
+
+  @Test
+  public void testConstants() {
+    assertThat(StackdriverExporterWorker.MAX_BATCH_EXPORT_SIZE).isEqualTo(200);
+    assertThat(StackdriverExporterWorker.CUSTOM_METRIC_DOMAIN).isEqualTo("custom.googleapis.com/");
+    assertThat(StackdriverExporterWorker.CUSTOM_OPENCENSUS_DOMAIN)
+        .isEqualTo("custom.googleapis.com/opencensus/");
+    assertThat(StackdriverExporterWorker.DEFAULT_DISPLAY_NAME_PREFIX).isEqualTo("OpenCensus/");
+  }
+
+  @Test
+  public void export() throws IOException {
+    View view =
+        View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+    ViewData viewData =
+        ViewData.create(
+            view,
+            ImmutableMap.of(Arrays.asList(VALUE), SumDataLong.create(1)),
+            CumulativeData.create(Timestamp.fromMillis(100), Timestamp.fromMillis(200)));
+    doReturn(ImmutableSet.of(view)).when(mockViewManager).getAllExportedViews();
+    doReturn(viewData).when(mockViewManager).getView(VIEW_NAME);
+
+    StackdriverExporterWorker worker =
+        new StackdriverExporterWorker(
+            PROJECT_ID,
+            new FakeMetricServiceClient(mockStub),
+            ONE_SECOND,
+            mockViewManager,
+            DEFAULT_RESOURCE,
+            null);
+    worker.export();
+
+    verify(mockStub, times(1)).createMetricDescriptorCallable();
+    verify(mockStub, times(1)).createTimeSeriesCallable();
+
+    MetricDescriptor descriptor =
+        StackdriverExportUtils.createMetricDescriptor(
+            view, PROJECT_ID, CUSTOM_OPENCENSUS_DOMAIN, DEFAULT_DISPLAY_NAME_PREFIX);
+    List<TimeSeries> timeSeries =
+        StackdriverExportUtils.createTimeSeriesList(
+            viewData, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN);
+    verify(mockCreateMetricDescriptorCallable, times(1))
+        .call(
+            eq(
+                CreateMetricDescriptorRequest.newBuilder()
+                    .setName("projects/" + PROJECT_ID)
+                    .setMetricDescriptor(descriptor)
+                    .build()));
+    verify(mockCreateTimeSeriesCallable, times(1))
+        .call(
+            eq(
+                CreateTimeSeriesRequest.newBuilder()
+                    .setName("projects/" + PROJECT_ID)
+                    .addAllTimeSeries(timeSeries)
+                    .build()));
+  }
+
+  @Test
+  public void doNotExportForEmptyViewData() {
+    View view =
+        View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+    ViewData empty =
+        ViewData.create(
+            view,
+            Collections.<List<TagValue>, AggregationData>emptyMap(),
+            CumulativeData.create(Timestamp.fromMillis(100), Timestamp.fromMillis(200)));
+    doReturn(ImmutableSet.of(view)).when(mockViewManager).getAllExportedViews();
+    doReturn(empty).when(mockViewManager).getView(VIEW_NAME);
+
+    StackdriverExporterWorker worker =
+        new StackdriverExporterWorker(
+            PROJECT_ID,
+            new FakeMetricServiceClient(mockStub),
+            ONE_SECOND,
+            mockViewManager,
+            DEFAULT_RESOURCE,
+            null);
+
+    worker.export();
+    verify(mockStub, times(1)).createMetricDescriptorCallable();
+    verify(mockStub, times(0)).createTimeSeriesCallable();
+  }
+
+  @Test
+  public void doNotExportIfFailedToRegisterView() {
+    View view =
+        View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+    doReturn(ImmutableSet.of(view)).when(mockViewManager).getAllExportedViews();
+    doThrow(new IllegalArgumentException()).when(mockStub).createMetricDescriptorCallable();
+    StackdriverExporterWorker worker =
+        new StackdriverExporterWorker(
+            PROJECT_ID,
+            new FakeMetricServiceClient(mockStub),
+            ONE_SECOND,
+            mockViewManager,
+            DEFAULT_RESOURCE,
+            null);
+
+    assertThat(worker.registerView(view)).isFalse();
+    worker.export();
+    verify(mockStub, times(1)).createMetricDescriptorCallable();
+    verify(mockStub, times(0)).createTimeSeriesCallable();
+  }
+
+  @Test
+  public void skipDifferentViewWithSameName() throws IOException {
+    StackdriverExporterWorker worker =
+        new StackdriverExporterWorker(
+            PROJECT_ID,
+            new FakeMetricServiceClient(mockStub),
+            ONE_SECOND,
+            mockViewManager,
+            DEFAULT_RESOURCE,
+            null);
+    View view1 =
+        View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+    assertThat(worker.registerView(view1)).isTrue();
+    verify(mockStub, times(1)).createMetricDescriptorCallable();
+
+    View view2 =
+        View.create(
+            VIEW_NAME,
+            "This is a different description.",
+            MEASURE,
+            SUM,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    assertThat(worker.registerView(view2)).isFalse();
+    verify(mockStub, times(1)).createMetricDescriptorCallable();
+  }
+
+  @Test
+  public void doNotCreateMetricDescriptorForRegisteredView() {
+    StackdriverExporterWorker worker =
+        new StackdriverExporterWorker(
+            PROJECT_ID,
+            new FakeMetricServiceClient(mockStub),
+            ONE_SECOND,
+            mockViewManager,
+            DEFAULT_RESOURCE,
+            null);
+    View view =
+        View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+    assertThat(worker.registerView(view)).isTrue();
+    verify(mockStub, times(1)).createMetricDescriptorCallable();
+
+    assertThat(worker.registerView(view)).isTrue();
+    verify(mockStub, times(1)).createMetricDescriptorCallable();
+  }
+
+  @Test
+  public void doNotCreateMetricDescriptorForIntervalView() {
+    StackdriverExporterWorker worker =
+        new StackdriverExporterWorker(
+            PROJECT_ID,
+            new FakeMetricServiceClient(mockStub),
+            ONE_SECOND,
+            mockViewManager,
+            DEFAULT_RESOURCE,
+            null);
+    View view =
+        View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), INTERVAL);
+    assertThat(worker.registerView(view)).isFalse();
+    verify(mockStub, times(0)).createMetricDescriptorCallable();
+  }
+
+  @Test
+  public void getDomain() {
+    assertThat(StackdriverExporterWorker.getDomain(null))
+        .isEqualTo("custom.googleapis.com/opencensus/");
+    assertThat(StackdriverExporterWorker.getDomain(""))
+        .isEqualTo("custom.googleapis.com/opencensus/");
+    assertThat(StackdriverExporterWorker.getDomain("custom.googleapis.com/myorg/"))
+        .isEqualTo("custom.googleapis.com/myorg/");
+    assertThat(StackdriverExporterWorker.getDomain("external.googleapis.com/prometheus/"))
+        .isEqualTo("external.googleapis.com/prometheus/");
+    assertThat(StackdriverExporterWorker.getDomain("myorg")).isEqualTo("myorg/");
+  }
+
+  @Test
+  public void getDisplayNamePrefix() {
+    assertThat(StackdriverExporterWorker.getDisplayNamePrefix(null)).isEqualTo("OpenCensus/");
+    assertThat(StackdriverExporterWorker.getDisplayNamePrefix("")).isEqualTo("");
+    assertThat(StackdriverExporterWorker.getDisplayNamePrefix("custom.googleapis.com/myorg/"))
+        .isEqualTo("custom.googleapis.com/myorg/");
+    assertThat(
+            StackdriverExporterWorker.getDisplayNamePrefix("external.googleapis.com/prometheus/"))
+        .isEqualTo("external.googleapis.com/prometheus/");
+    assertThat(StackdriverExporterWorker.getDisplayNamePrefix("myorg")).isEqualTo("myorg/");
+  }
+
+  /*
+   * MetricServiceClient.createMetricDescriptor() and MetricServiceClient.createTimeSeries() are
+   * final methods and cannot be mocked. We have to use a mock MetricServiceStub in order to verify
+   * the output.
+   */
+  private static final class FakeMetricServiceClient extends MetricServiceClient {
+
+    protected FakeMetricServiceClient(MetricServiceStub stub) {
+      super(stub);
+    }
+  }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java
new file mode 100644
index 0000000..2d5eba1
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.api.MonitoredResource;
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import io.opencensus.common.Duration;
+import java.util.Date;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverStatsConfiguration}. */
+@RunWith(JUnit4.class)
+public class StackdriverStatsConfigurationTest {
+
+  private static final Credentials FAKE_CREDENTIALS =
+      GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+  private static final String PROJECT_ID = "project";
+  private static final Duration DURATION = Duration.create(10, 0);
+  private static final MonitoredResource RESOURCE =
+      MonitoredResource.newBuilder()
+          .setType("gce-instance")
+          .putLabels("instance-id", "instance")
+          .build();
+  private static final String CUSTOM_PREFIX = "myorg";
+
+  @Test
+  public void testBuild() {
+    StackdriverStatsConfiguration configuration =
+        StackdriverStatsConfiguration.builder()
+            .setCredentials(FAKE_CREDENTIALS)
+            .setProjectId(PROJECT_ID)
+            .setExportInterval(DURATION)
+            .setMonitoredResource(RESOURCE)
+            .setMetricNamePrefix(CUSTOM_PREFIX)
+            .build();
+    assertThat(configuration.getCredentials()).isEqualTo(FAKE_CREDENTIALS);
+    assertThat(configuration.getProjectId()).isEqualTo(PROJECT_ID);
+    assertThat(configuration.getExportInterval()).isEqualTo(DURATION);
+    assertThat(configuration.getMonitoredResource()).isEqualTo(RESOURCE);
+    assertThat(configuration.getMetricNamePrefix()).isEqualTo(CUSTOM_PREFIX);
+  }
+
+  @Test
+  public void testBuild_Default() {
+    StackdriverStatsConfiguration configuration = StackdriverStatsConfiguration.builder().build();
+    assertThat(configuration.getCredentials()).isNull();
+    assertThat(configuration.getProjectId()).isNull();
+    assertThat(configuration.getExportInterval()).isNull();
+    assertThat(configuration.getMonitoredResource()).isNull();
+    assertThat(configuration.getMetricNamePrefix()).isNull();
+  }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java
new file mode 100644
index 0000000..f5e3edd
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import io.opencensus.common.Duration;
+import java.io.IOException;
+import java.util.Date;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverStatsExporter}. */
+@RunWith(JUnit4.class)
+public class StackdriverStatsExporterTest {
+
+  private static final String PROJECT_ID = "projectId";
+  private static final Duration ONE_SECOND = Duration.create(1, 0);
+  private static final Duration NEG_ONE_SECOND = Duration.create(-1, 0);
+  private static final Credentials FAKE_CREDENTIALS =
+      GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+  private static final StackdriverStatsConfiguration CONFIGURATION =
+      StackdriverStatsConfiguration.builder()
+          .setCredentials(FAKE_CREDENTIALS)
+          .setProjectId("project")
+          .build();
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testConstants() {
+    assertThat(StackdriverStatsExporter.DEFAULT_INTERVAL).isEqualTo(Duration.create(60, 0));
+  }
+
+  @Test
+  public void createWithNullStackdriverStatsConfiguration() throws IOException {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("configuration");
+    StackdriverStatsExporter.createAndRegister((StackdriverStatsConfiguration) null);
+  }
+
+  @Test
+  public void createWithNegativeDuration_WithConfiguration() throws IOException {
+    StackdriverStatsConfiguration configuration =
+        StackdriverStatsConfiguration.builder()
+            .setCredentials(FAKE_CREDENTIALS)
+            .setExportInterval(NEG_ONE_SECOND)
+            .build();
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Duration must be positive");
+    StackdriverStatsExporter.createAndRegister(configuration);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void createWithNullCredentials() throws IOException {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("credentials");
+    StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+        null, PROJECT_ID, ONE_SECOND);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void createWithNullProjectId() throws IOException {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("projectId");
+    StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+        GoogleCredentials.newBuilder().build(), null, ONE_SECOND);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void createWithNullDuration() throws IOException {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("exportInterval");
+    StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+        GoogleCredentials.newBuilder().build(), PROJECT_ID, null);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void createWithNegativeDuration() throws IOException {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Duration must be positive");
+    StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+        GoogleCredentials.newBuilder().build(), PROJECT_ID, NEG_ONE_SECOND);
+  }
+
+  @Test
+  public void createExporterTwice() throws IOException {
+    StackdriverStatsExporter.createAndRegister(CONFIGURATION);
+    try {
+      thrown.expect(IllegalStateException.class);
+      thrown.expectMessage("Stackdriver stats exporter is already created.");
+      StackdriverStatsExporter.createAndRegister(CONFIGURATION);
+    } finally {
+      StackdriverStatsExporter.unsafeResetExporter();
+    }
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void createWithNullMonitoredResource() throws IOException {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("monitoredResource");
+    StackdriverStatsExporter.createAndRegisterWithMonitoredResource(ONE_SECOND, null);
+  }
+}
diff --git a/exporters/trace/instana/README.md b/exporters/trace/instana/README.md
new file mode 100644
index 0000000..22ace22
--- /dev/null
+++ b/exporters/trace/instana/README.md
@@ -0,0 +1,73 @@
+# OpenCensus Instana Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Instana Trace Exporter* is a trace exporter that exports
+data to Instana. [Instana](http://www.instana.com/) is a distributed
+tracing system.
+
+## Quickstart
+
+### Prerequisites
+
+[Instana](http://www.instana.com/) forwards traces exported by applications
+instrumented with Census to its backend using the Instana agent processes as proxy.
+If the agent is used on the same host as Census, please take care to deactivate
+automatic tracing.
+
+
+### Hello Stan
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-exporter-trace-instana</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-instana:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) throws Exception {
+    InstanaTraceExporter.createAndRegister("http://localhost:42699/com.instana.plugin.generic.trace");
+    // ...
+  }
+}
+```
+
+#### Java Versions
+
+Java 6 or above is required for using this exporter.
+
+[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-exporter-trace-instana/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-instana
diff --git a/exporters/trace/instana/build.gradle b/exporters/trace/instana/build.gradle
new file mode 100644
index 0000000..028bc20
--- /dev/null
+++ b/exporters/trace/instana/build.gradle
@@ -0,0 +1,16 @@
+description = 'OpenCensus Trace Instana Exporter'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.guava
+
+    testCompile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java
new file mode 100644
index 0000000..649a026
--- /dev/null
+++ b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2018, 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.exporter.trace.instana;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.io.BaseEncoding;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/*
+ * Exports to an Instana agent acting as proxy to the Instana backend (and handling authentication)
+ * Uses the Trace SDK documented:
+ * https://github.com/instana/instana-java-sdk#instana-trace-webservice
+ *
+ * Currently does a blocking export using HttpUrlConnection.
+ * Also uses a StringBuilder to build JSON.
+ * Both can be improved should 3rd party library usage not be a concern.
+ *
+ * Major TODO is the limitation of Instana to only suport 64bit trace ids, which will be resolved.
+ * Until then it is crossing fingers and treating it as 50% sampler :).
+ */
+final class InstanaExporterHandler extends SpanExporter.Handler {
+
+  private static final Tracer tracer = Tracing.getTracer();
+  private static final Sampler probabilitySpampler = Samplers.probabilitySampler(0.0001);
+  private final URL agentEndpoint;
+
+  InstanaExporterHandler(URL agentEndpoint) {
+    this.agentEndpoint = agentEndpoint;
+  }
+
+  private static String encodeTraceId(TraceId traceId) {
+    return BaseEncoding.base16().lowerCase().encode(traceId.getBytes(), 0, 8);
+  }
+
+  private static String encodeSpanId(SpanId spanId) {
+    return BaseEncoding.base16().lowerCase().encode(spanId.getBytes());
+  }
+
+  private static String toSpanName(SpanData spanData) {
+    return spanData.getName();
+  }
+
+  private static String toSpanType(SpanData spanData) {
+    if (spanData.getKind() == Kind.SERVER
+        || (spanData.getKind() == null
+            && (spanData.getParentSpanId() == null
+                || Boolean.TRUE.equals(spanData.getHasRemoteParent())))) {
+      return "ENTRY";
+    }
+
+    // This is a hack because the Span API did not have SpanKind.
+    if (spanData.getKind() == Kind.CLIENT
+        || (spanData.getKind() == null && spanData.getName().startsWith("Sent."))) {
+      return "EXIT";
+    }
+
+    return "INTERMEDIATE";
+  }
+
+  private static long toMillis(Timestamp timestamp) {
+    return SECONDS.toMillis(timestamp.getSeconds()) + NANOSECONDS.toMillis(timestamp.getNanos());
+  }
+
+  private static long toMillis(Timestamp start, Timestamp end) {
+    Duration duration = end.subtractTimestamp(start);
+    return SECONDS.toMillis(duration.getSeconds()) + NANOSECONDS.toMillis(duration.getNanos());
+  }
+
+  // The return type needs to be nullable when this function is used as an argument to 'match' in
+  // attributeValueToString, because 'match' doesn't allow covariant return types.
+  private static final Function<Object, /*@Nullable*/ String> returnToString =
+      Functions.returnToString();
+
+  @javax.annotation.Nullable
+  private static String attributeValueToString(AttributeValue attributeValue) {
+    return attributeValue.match(
+        returnToString,
+        returnToString,
+        returnToString,
+        returnToString,
+        Functions.</*@Nullable*/ String>returnNull());
+  }
+
+  static String convertToJson(Collection<SpanData> spanDataList) {
+    StringBuilder sb = new StringBuilder();
+    sb.append('[');
+    for (final SpanData span : spanDataList) {
+      final SpanContext spanContext = span.getContext();
+      final SpanId parentSpanId = span.getParentSpanId();
+      final Timestamp startTimestamp = span.getStartTimestamp();
+      final Timestamp endTimestamp = span.getEndTimestamp();
+      final Status status = span.getStatus();
+      if (status == null || endTimestamp == null) {
+        continue;
+      }
+      if (sb.length() > 1) {
+        sb.append(',');
+      }
+      sb.append('{');
+      sb.append("\"spanId\":\"").append(encodeSpanId(spanContext.getSpanId())).append("\",");
+      sb.append("\"traceId\":\"").append(encodeTraceId(spanContext.getTraceId())).append("\",");
+      if (parentSpanId != null) {
+        sb.append("\"parentId\":\"").append(encodeSpanId(parentSpanId)).append("\",");
+      }
+      sb.append("\"timestamp\":").append(toMillis(startTimestamp)).append(',');
+      sb.append("\"duration\":").append(toMillis(startTimestamp, endTimestamp)).append(',');
+      sb.append("\"name\":\"").append(toSpanName(span)).append("\",");
+      sb.append("\"type\":\"").append(toSpanType(span)).append('"');
+      if (!status.isOk()) {
+        sb.append(",\"error\":").append("true");
+      }
+      Map<String, AttributeValue> attributeMap = span.getAttributes().getAttributeMap();
+      if (attributeMap.size() > 0) {
+        StringBuilder dataSb = new StringBuilder();
+        dataSb.append('{');
+        for (Entry<String, AttributeValue> entry : attributeMap.entrySet()) {
+          if (dataSb.length() > 1) {
+            dataSb.append(',');
+          }
+          dataSb
+              .append("\"")
+              .append(entry.getKey())
+              .append("\":\"")
+              .append(attributeValueToString(entry.getValue()))
+              .append("\"");
+        }
+        dataSb.append('}');
+
+        sb.append(",\"data\":").append(dataSb);
+      }
+      sb.append('}');
+    }
+    sb.append(']');
+    return sb.toString();
+  }
+
+  @Override
+  public void export(Collection<SpanData> spanDataList) {
+    // Start a new span with explicit 1/10000 sampling probability to avoid the case when user
+    // sets the default sampler to always sample and we get the gRPC span of the instana
+    // export call always sampled and go to an infinite loop.
+    Scope scope =
+        tracer.spanBuilder("ExportInstanaTraces").setSampler(probabilitySpampler).startScopedSpan();
+    try {
+      String json = convertToJson(spanDataList);
+
+      OutputStream outputStream = null;
+      InputStream inputStream = null;
+      try {
+        HttpURLConnection connection = (HttpURLConnection) agentEndpoint.openConnection();
+        connection.setRequestMethod("POST");
+        connection.setDoOutput(true);
+        outputStream = connection.getOutputStream();
+        outputStream.write(json.getBytes(Charset.defaultCharset()));
+        outputStream.flush();
+        inputStream = connection.getInputStream();
+        if (connection.getResponseCode() != 200) {
+          tracer
+              .getCurrentSpan()
+              .setStatus(
+                  Status.UNKNOWN.withDescription("Response " + connection.getResponseCode()));
+        }
+      } catch (IOException e) {
+        tracer
+            .getCurrentSpan()
+            .setStatus(
+                Status.UNKNOWN.withDescription(
+                    e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
+        // dropping span batch
+      } finally {
+        if (inputStream != null) {
+          try {
+            inputStream.close();
+          } catch (IOException e) {
+            // ignore
+          }
+        }
+        if (outputStream != null) {
+          try {
+            outputStream.close();
+          } catch (IOException e) {
+            // ignore
+          }
+        }
+      }
+    } finally {
+      scope.close();
+    }
+  }
+}
diff --git a/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java
new file mode 100644
index 0000000..da2ce35
--- /dev/null
+++ b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018, 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.exporter.trace.instana;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.net.MalformedURLException;
+import java.net.URL;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Instana.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ *   InstanaTraceExporter.createAndRegister("http://localhost:42699/com.instana.plugin.generic.trace");
+ *   ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+public final class InstanaTraceExporter {
+
+  private static final String REGISTER_NAME = InstanaTraceExporter.class.getName();
+  private static final Object monitor = new Object();
+
+  @GuardedBy("monitor")
+  @Nullable
+  private static Handler handler = null;
+
+  private InstanaTraceExporter() {}
+
+  /**
+   * Creates and registers the Instana Trace exporter to the OpenCensus library. Only one Instana
+   * exporter can be registered at any point.
+   *
+   * @param agentEndpoint Ex http://localhost:42699/com.instana.plugin.generic.trace
+   * @throws MalformedURLException if the agentEndpoint is not a valid http url.
+   * @throws IllegalStateException if a Instana exporter is already registered.
+   * @since 0.12
+   */
+  public static void createAndRegister(String agentEndpoint) throws MalformedURLException {
+    synchronized (monitor) {
+      checkState(handler == null, "Instana exporter is already registered.");
+      Handler newHandler = new InstanaExporterHandler(new URL(agentEndpoint));
+      handler = newHandler;
+      register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+    }
+  }
+
+  /**
+   * Registers the {@code InstanaTraceExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+   */
+  @VisibleForTesting
+  static void register(SpanExporter spanExporter, Handler handler) {
+    spanExporter.registerHandler(REGISTER_NAME, handler);
+  }
+
+  /**
+   * Unregisters the Instana Trace exporter from the OpenCensus library.
+   *
+   * @throws IllegalStateException if a Instana exporter is not registered.
+   * @since 0.12
+   */
+  public static void unregister() {
+    synchronized (monitor) {
+      checkState(handler != null, "Instana exporter is not registered.");
+      unregister(Tracing.getExportComponent().getSpanExporter());
+      handler = null;
+    }
+  }
+
+  /**
+   * Unregisters the {@code InstanaTraceExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+   *     unregistered.
+   */
+  @VisibleForTesting
+  static void unregister(SpanExporter spanExporter) {
+    spanExporter.unregisterHandler(REGISTER_NAME);
+  }
+}
diff --git a/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java
new file mode 100644
index 0000000..3b5e119
--- /dev/null
+++ b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2018, 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.exporter.trace.instana;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.Attributes;
+import io.opencensus.trace.export.SpanData.Links;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InstanaExporterHandler}. */
+@RunWith(JUnit4.class)
+public class InstanaExporterHandlerTest {
+  private static final String TRACE_ID = "d239036e7d5cec116b562147388b35bf";
+  private static final String SPAN_ID = "9cc1e3049173be09";
+  private static final String PARENT_SPAN_ID = "8b03ab423da481c5";
+  private static final Map<String, AttributeValue> attributes =
+      ImmutableMap.of("http.url", AttributeValue.stringAttributeValue("http://localhost/foo"));
+  private static final List<TimedEvent<Annotation>> annotations = Collections.emptyList();
+  private static final List<TimedEvent<MessageEvent>> messageEvents =
+      ImmutableList.of(
+          TimedEvent.create(
+              Timestamp.create(1505855799, 433901068),
+              MessageEvent.builder(Type.RECEIVED, 0).setCompressedMessageSize(7).build()),
+          TimedEvent.create(
+              Timestamp.create(1505855799, 459486280),
+              MessageEvent.builder(Type.SENT, 0).setCompressedMessageSize(13).build()));
+
+  @Test
+  public void generateSpan_NoKindAndRemoteParent() {
+    SpanData data =
+        SpanData.create(
+            SpanContext.create(
+                TraceId.fromLowerBase16(TRACE_ID),
+                SpanId.fromLowerBase16(SPAN_ID),
+                TraceOptions.builder().setIsSampled(true).build()),
+            SpanId.fromLowerBase16(PARENT_SPAN_ID),
+            true, /* hasRemoteParent */
+            "SpanName", /* name */
+            null, /* kind */
+            Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+            Attributes.create(attributes, 0 /* droppedAttributesCount */),
+            TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+            TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+            Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+            null, /* childSpanCount */
+            Status.OK,
+            Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+    assertThat(InstanaExporterHandler.convertToJson(Collections.singletonList(data)))
+        .isEqualTo(
+            "["
+                + "{"
+                + "\"spanId\":\"9cc1e3049173be09\","
+                + "\"traceId\":\"d239036e7d5cec11\","
+                + "\"parentId\":\"8b03ab423da481c5\","
+                + "\"timestamp\":1505855794194,"
+                + "\"duration\":5271,"
+                + "\"name\":\"SpanName\","
+                + "\"type\":\"ENTRY\","
+                + "\"data\":"
+                + "{\"http.url\":\"http://localhost/foo\"}"
+                + "}"
+                + "]");
+  }
+
+  @Test
+  public void generateSpan_ServerKind() {
+    SpanData data =
+        SpanData.create(
+            SpanContext.create(
+                TraceId.fromLowerBase16(TRACE_ID),
+                SpanId.fromLowerBase16(SPAN_ID),
+                TraceOptions.builder().setIsSampled(true).build()),
+            SpanId.fromLowerBase16(PARENT_SPAN_ID),
+            true, /* hasRemoteParent */
+            "SpanName", /* name */
+            Kind.SERVER, /* kind */
+            Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+            Attributes.create(attributes, 0 /* droppedAttributesCount */),
+            TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+            TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+            Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+            null, /* childSpanCount */
+            Status.OK,
+            Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+    assertThat(InstanaExporterHandler.convertToJson(Collections.singletonList(data)))
+        .isEqualTo(
+            "["
+                + "{"
+                + "\"spanId\":\"9cc1e3049173be09\","
+                + "\"traceId\":\"d239036e7d5cec11\","
+                + "\"parentId\":\"8b03ab423da481c5\","
+                + "\"timestamp\":1505855794194,"
+                + "\"duration\":5271,"
+                + "\"name\":\"SpanName\","
+                + "\"type\":\"ENTRY\","
+                + "\"data\":"
+                + "{\"http.url\":\"http://localhost/foo\"}"
+                + "}"
+                + "]");
+  }
+
+  @Test
+  public void generateSpan_ClientKind() {
+    SpanData data =
+        SpanData.create(
+            SpanContext.create(
+                TraceId.fromLowerBase16(TRACE_ID),
+                SpanId.fromLowerBase16(SPAN_ID),
+                TraceOptions.builder().setIsSampled(true).build()),
+            SpanId.fromLowerBase16(PARENT_SPAN_ID),
+            true, /* hasRemoteParent */
+            "SpanName", /* name */
+            Kind.CLIENT, /* kind */
+            Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+            Attributes.create(attributes, 0 /* droppedAttributesCount */),
+            TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+            TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+            Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+            null, /* childSpanCount */
+            Status.OK,
+            Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+    assertThat(InstanaExporterHandler.convertToJson(Collections.singletonList(data)))
+        .isEqualTo(
+            "["
+                + "{"
+                + "\"spanId\":\"9cc1e3049173be09\","
+                + "\"traceId\":\"d239036e7d5cec11\","
+                + "\"parentId\":\"8b03ab423da481c5\","
+                + "\"timestamp\":1505855794194,"
+                + "\"duration\":5271,"
+                + "\"name\":\"SpanName\","
+                + "\"type\":\"EXIT\","
+                + "\"data\":"
+                + "{\"http.url\":\"http://localhost/foo\"}"
+                + "}"
+                + "]");
+  }
+}
diff --git a/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java
new file mode 100644
index 0000000..a4d03df
--- /dev/null
+++ b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.exporter.trace.instana;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link InstanaTraceExporter}. */
+@RunWith(JUnit4.class)
+public class InstanaTraceExporterTest {
+
+  @Mock private SpanExporter spanExporter;
+  @Mock private Handler handler;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void registerUnregisterInstanaExporter() {
+    InstanaTraceExporter.register(spanExporter, handler);
+    verify(spanExporter)
+        .registerHandler(
+            eq("io.opencensus.exporter.trace.instana.InstanaTraceExporter"), same(handler));
+    InstanaTraceExporter.unregister(spanExporter);
+    verify(spanExporter)
+        .unregisterHandler(eq("io.opencensus.exporter.trace.instana.InstanaTraceExporter"));
+  }
+}
diff --git a/exporters/trace/jaeger/README.md b/exporters/trace/jaeger/README.md
new file mode 100644
index 0000000..7a5b68e
--- /dev/null
+++ b/exporters/trace/jaeger/README.md
@@ -0,0 +1,90 @@
+# OpenCensus Jaeger Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Jaeger Trace Exporter* is a trace exporter that exports
+data to Jaeger.
+
+[Jaeger](https://jaeger.readthedocs.io/en/latest/), inspired by [Dapper](https://research.google.com/pubs/pub36356.html) and [OpenZipkin](http://zipkin.io/), is a distributed tracing system released as open source by [Uber Technologies](http://uber.github.io/). It is used for monitoring and troubleshooting microservices-based distributed systems, including:
+
+- Distributed context propagation
+- Distributed transaction monitoring
+- Root cause analysis
+- Service dependency analysis
+- Performance / latency optimization
+
+## Quickstart
+
+### Prerequisites
+
+[Jaeger](https://jaeger.readthedocs.io/en/latest/) stores and queries traces exported by
+applications instrumented with Census. The easiest way to [start a Jaeger
+server](https://jaeger.readthedocs.io/en/latest/getting_started/) is to paste the below:
+
+```bash
+docker run -d \
+    -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
+    -p5775:5775/udp -p6831:6831/udp -p6832:6832/udp \
+    -p5778:5778 -p16686:16686 -p14268:14268 -p9411:9411 \
+  jaegertracing/all-in-one:latest
+```
+
+### Hello Jaeger
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-exporter-trace-jaeger</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-jaeger:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This will export traces to the Jaeger thrift format to the Jaeger instance started previously:
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) throws Exception {
+    JaegerTraceExporter.createAndRegister("http://127.0.0.1:14268/api/traces", "my-service");
+    // ...
+  }
+}
+```
+
+See also [this integration test](https://github.com/census-instrumentation/opencensus-java/blob/master/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java).
+
+#### Java Versions
+
+Java 6 or above is required for using this exporter.
+
+[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-exporter-trace-jaeger/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-jaeger
diff --git a/exporters/trace/jaeger/build.gradle b/exporters/trace/jaeger/build.gradle
new file mode 100644
index 0000000..04829aa
--- /dev/null
+++ b/exporters/trace/jaeger/build.gradle
@@ -0,0 +1,37 @@
+description = 'OpenCensus Trace Jaeger Exporter'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+// Docker tests require JDK 8+
+sourceSets {
+    test {
+        java {
+            if (!JavaVersion.current().isJava8Compatible()) {
+                exclude '**/JaegerExporterHandlerIntegrationTest.java'
+            }
+        }
+    }
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.guava
+
+    compile(libraries.jaeger_reporter) {
+        // Prefer library version.
+        exclude group: 'com.google.guava', module: 'guava'
+    }
+
+    testCompile project(':opencensus-api'),
+            'org.testcontainers:testcontainers:1.7.0',
+            'com.google.http-client:google-http-client-gson:1.23.0'
+
+    // Unless linked to impl, spans will be blank and not exported during integration tests.
+    testRuntime project(':opencensus-impl')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java
new file mode 100644
index 0000000..e0a1629
--- /dev/null
+++ b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+import com.google.errorprone.annotations.MustBeClosed;
+import com.uber.jaeger.exceptions.SenderException;
+import com.uber.jaeger.senders.HttpSender;
+import com.uber.jaeger.thriftjava.Log;
+import com.uber.jaeger.thriftjava.Process;
+import com.uber.jaeger.thriftjava.Span;
+import com.uber.jaeger.thriftjava.SpanRef;
+import com.uber.jaeger.thriftjava.SpanRefType;
+import com.uber.jaeger.thriftjava.Tag;
+import com.uber.jaeger.thriftjava.TagType;
+import io.opencensus.common.Function;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+final class JaegerExporterHandler extends SpanExporter.Handler {
+  private static final String EXPORT_SPAN_NAME = "ExportJaegerTraces";
+  private static final String DESCRIPTION = "description";
+
+  private static final Logger logger = Logger.getLogger(JaegerExporterHandler.class.getName());
+
+  /**
+   * Sampler with low probability used during the export in order to avoid the case when user sets
+   * the default sampler to always sample and we get the Thrift span of the Jaeger export call
+   * always sampled and go to an infinite loop.
+   */
+  private static final Sampler lowProbabilitySampler = Samplers.probabilitySampler(0.0001);
+
+  private static final Tracer tracer = Tracing.getTracer();
+
+  private static final Function<? super String, Tag> stringAttributeConverter =
+      new Function<String, Tag>() {
+        @Override
+        public Tag apply(final String value) {
+          final Tag tag = new Tag();
+          tag.setVType(TagType.STRING);
+          tag.setVStr(value);
+          return tag;
+        }
+      };
+
+  private static final Function<? super Boolean, Tag> booleanAttributeConverter =
+      new Function<Boolean, Tag>() {
+        @Override
+        public Tag apply(final Boolean value) {
+          final Tag tag = new Tag();
+          tag.setVType(TagType.BOOL);
+          tag.setVBool(value);
+          return tag;
+        }
+      };
+
+  private static final Function<? super Double, Tag> doubleAttributeConverter =
+      new Function<Double, Tag>() {
+        @Override
+        public Tag apply(final Double value) {
+          final Tag tag = new Tag();
+          tag.setVType(TagType.DOUBLE);
+          tag.setVDouble(value);
+          return tag;
+        }
+      };
+
+  private static final Function<? super Long, Tag> longAttributeConverter =
+      new Function<Long, Tag>() {
+        @Override
+        public Tag apply(final Long value) {
+          final Tag tag = new Tag();
+          tag.setVType(TagType.LONG);
+          tag.setVLong(value);
+          return tag;
+        }
+      };
+
+  private static final Function<Object, Tag> defaultAttributeConverter =
+      new Function<Object, Tag>() {
+        @Override
+        public Tag apply(final Object value) {
+          final Tag tag = new Tag();
+          tag.setVType(TagType.STRING);
+          tag.setVStr(value.toString());
+          return tag;
+        }
+      };
+
+  // Re-usable buffers to avoid too much memory allocation during conversions.
+  // N.B.: these make instances of this class thread-unsafe, hence the above
+  // @NotThreadSafe annotation.
+  private final byte[] spanIdBuffer = new byte[SpanId.SIZE];
+  private final byte[] traceIdBuffer = new byte[TraceId.SIZE];
+  private final byte[] optionsBuffer = new byte[Integer.SIZE / Byte.SIZE];
+
+  private final HttpSender sender;
+  private final Process process;
+
+  JaegerExporterHandler(final HttpSender sender, final Process process) {
+    this.sender = checkNotNull(sender, "Jaeger sender must NOT be null.");
+    this.process = checkNotNull(process, "Process sending traces must NOT be null.");
+  }
+
+  @Override
+  public void export(final Collection<SpanData> spanDataList) {
+    final Scope exportScope = newExportScope();
+    try {
+      doExport(spanDataList);
+    } catch (SenderException e) {
+      tracer
+          .getCurrentSpan() // exportScope above.
+          .setStatus(Status.UNKNOWN.withDescription(getMessageOrDefault(e)));
+      logger.log(Level.WARNING, "Failed to export traces to Jaeger: " + e);
+    } finally {
+      exportScope.close();
+    }
+  }
+
+  @MustBeClosed
+  private static Scope newExportScope() {
+    // Start a new span with explicit sampler (with low probability) to avoid the case when user
+    // sets the default sampler to always sample and we get the Thrift span of the Jaeger
+    // export call always sampled and go to an infinite loop.
+    return tracer.spanBuilder(EXPORT_SPAN_NAME).setSampler(lowProbabilitySampler).startScopedSpan();
+  }
+
+  private void doExport(final Collection<SpanData> spanDataList) throws SenderException {
+    final List<Span> spans = spanDataToJaegerThriftSpans(spanDataList);
+    sender.send(process, spans);
+  }
+
+  private static String getMessageOrDefault(final SenderException e) {
+    return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
+  }
+
+  private List<Span> spanDataToJaegerThriftSpans(final Collection<SpanData> spanDataList) {
+    final List<Span> spans = Lists.newArrayListWithExpectedSize(spanDataList.size());
+    for (final SpanData spanData : spanDataList) {
+      spans.add(spanDataToJaegerThriftSpan(spanData));
+    }
+    return spans;
+  }
+
+  private Span spanDataToJaegerThriftSpan(final SpanData spanData) {
+    final long startTimeInMicros = timestampToMicros(spanData.getStartTimestamp());
+    final long endTimeInMicros = timestampToMicros(spanData.getEndTimestamp());
+
+    final SpanContext context = spanData.getContext();
+    copyToBuffer(context.getTraceId());
+
+    return new com.uber.jaeger.thriftjava.Span(
+            traceIdLow(),
+            traceIdHigh(),
+            spanIdToLong(context.getSpanId()),
+            spanIdToLong(spanData.getParentSpanId()),
+            spanData.getName(),
+            optionsToFlags(context.getTraceOptions()),
+            startTimeInMicros,
+            endTimeInMicros - startTimeInMicros)
+        .setReferences(linksToReferences(spanData.getLinks().getLinks()))
+        .setTags(attributesToTags(spanData.getAttributes().getAttributeMap()))
+        .setLogs(annotationEventsToLogs(spanData.getAnnotations().getEvents()));
+  }
+
+  private void copyToBuffer(final TraceId traceId) {
+    // Attempt to minimise allocations, since TraceId#getBytes currently creates a defensive copy:
+    traceId.copyBytesTo(traceIdBuffer, 0);
+  }
+
+  private long traceIdHigh() {
+    return Longs.fromBytes(
+        traceIdBuffer[0],
+        traceIdBuffer[1],
+        traceIdBuffer[2],
+        traceIdBuffer[3],
+        traceIdBuffer[4],
+        traceIdBuffer[5],
+        traceIdBuffer[6],
+        traceIdBuffer[7]);
+  }
+
+  private long traceIdLow() {
+    return Longs.fromBytes(
+        traceIdBuffer[8],
+        traceIdBuffer[9],
+        traceIdBuffer[10],
+        traceIdBuffer[11],
+        traceIdBuffer[12],
+        traceIdBuffer[13],
+        traceIdBuffer[14],
+        traceIdBuffer[15]);
+  }
+
+  private long spanIdToLong(final @Nullable SpanId spanId) {
+    if (spanId == null) {
+      return 0L;
+    }
+    // Attempt to minimise allocations, since SpanId#getBytes currently creates a defensive copy:
+    spanId.copyBytesTo(spanIdBuffer, 0);
+    return Longs.fromByteArray(spanIdBuffer);
+  }
+
+  private int optionsToFlags(final TraceOptions traceOptions) {
+    // Attempt to minimise allocations, since TraceOptions#getBytes currently creates a defensive
+    // copy:
+    traceOptions.copyBytesTo(optionsBuffer, optionsBuffer.length - 1);
+    return Ints.fromByteArray(optionsBuffer);
+  }
+
+  private List<SpanRef> linksToReferences(final List<Link> links) {
+    final List<SpanRef> spanRefs = Lists.newArrayListWithExpectedSize(links.size());
+    for (final Link link : links) {
+      copyToBuffer(link.getTraceId());
+      spanRefs.add(
+          new SpanRef(
+              linkTypeToRefType(link.getType()),
+              traceIdLow(),
+              traceIdHigh(),
+              spanIdToLong(link.getSpanId())));
+    }
+    return spanRefs;
+  }
+
+  private static long timestampToMicros(final @Nullable Timestamp timestamp) {
+    return (timestamp == null)
+        ? 0L
+        : SECONDS.toMicros(timestamp.getSeconds()) + NANOSECONDS.toMicros(timestamp.getNanos());
+  }
+
+  private static SpanRefType linkTypeToRefType(final Link.Type type) {
+    switch (type) {
+      case CHILD_LINKED_SPAN:
+        return SpanRefType.CHILD_OF;
+      case PARENT_LINKED_SPAN:
+        return SpanRefType.FOLLOWS_FROM;
+    }
+    throw new UnsupportedOperationException(
+        format("Failed to convert link type [%s] to a Jaeger SpanRefType.", type));
+  }
+
+  private static List<Tag> attributesToTags(final Map<String, AttributeValue> attributes) {
+    final List<Tag> tags = Lists.newArrayListWithExpectedSize(attributes.size());
+    for (final Map.Entry<String, AttributeValue> entry : attributes.entrySet()) {
+      final Tag tag =
+          entry
+              .getValue()
+              .match(
+                  stringAttributeConverter,
+                  booleanAttributeConverter,
+                  longAttributeConverter,
+                  doubleAttributeConverter,
+                  defaultAttributeConverter);
+      tag.setKey(entry.getKey());
+      tags.add(tag);
+    }
+    return tags;
+  }
+
+  private static List<Log> annotationEventsToLogs(
+      final List<SpanData.TimedEvent<Annotation>> events) {
+    final List<Log> logs = Lists.newArrayListWithExpectedSize(events.size());
+    for (final SpanData.TimedEvent<Annotation> event : events) {
+      final long timestampsInMicros = timestampToMicros(event.getTimestamp());
+      final List<Tag> tags = attributesToTags(event.getEvent().getAttributes());
+      tags.add(descriptionToTag(event.getEvent().getDescription()));
+      final Log log = new Log(timestampsInMicros, tags);
+      logs.add(log);
+    }
+    return logs;
+  }
+
+  private static Tag descriptionToTag(final String description) {
+    final Tag tag = new Tag(DESCRIPTION, TagType.STRING);
+    tag.setVStr(description);
+    return tag;
+  }
+}
diff --git a/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java
new file mode 100644
index 0000000..4890f01
--- /dev/null
+++ b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.uber.jaeger.senders.HttpSender;
+import com.uber.jaeger.thriftjava.Process;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Jaeger. Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ *   JaegerTraceExporter.createAndRegister("http://127.0.0.1:14268/api/traces", "myservicename");
+ *     ... // Do work.
+ *   }
+ * }</pre>
+ *
+ * @since 0.13
+ */
+public final class JaegerTraceExporter {
+  private static final String REGISTER_NAME = JaegerTraceExporter.class.getName();
+  private static final Object monitor = new Object();
+
+  @GuardedBy("monitor")
+  @Nullable
+  private static SpanExporter.Handler handler = null;
+
+  // Make constructor private to hide it from the API and therefore avoid users calling it.
+  private JaegerTraceExporter() {}
+
+  /**
+   * Creates and registers the Jaeger Trace exporter to the OpenCensus library. Only one Jaeger
+   * exporter can be registered at any point.
+   *
+   * @param thriftEndpoint the Thrift endpoint of your Jaeger instance, e.g.:
+   *     "http://127.0.0.1:14268/api/traces"
+   * @param serviceName the local service name of the process.
+   * @throws IllegalStateException if a Jaeger exporter is already registered.
+   * @since 0.13
+   */
+  public static void createAndRegister(final String thriftEndpoint, final String serviceName) {
+    synchronized (monitor) {
+      checkState(handler == null, "Jaeger exporter is already registered.");
+      final SpanExporter.Handler newHandler = newHandler(thriftEndpoint, serviceName);
+      JaegerTraceExporter.handler = newHandler;
+      register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+    }
+  }
+
+  /**
+   * Creates and registers the Jaeger Trace exporter to the OpenCensus library using the provided
+   * HttpSender. Only one Jaeger exporter can be registered at any point.
+   *
+   * @param httpSender the pre-configured HttpSender to use with the exporter
+   * @param serviceName the local service name of the process.
+   * @throws IllegalStateException if a Jaeger exporter is already registered.
+   * @since 0.17
+   */
+  public static void createWithSender(final HttpSender httpSender, final String serviceName) {
+    synchronized (monitor) {
+      checkState(handler == null, "Jaeger exporter is already registered.");
+      final SpanExporter.Handler newHandler = newHandlerWithSender(httpSender, serviceName);
+      JaegerTraceExporter.handler = newHandler;
+      register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+    }
+  }
+
+  private static SpanExporter.Handler newHandler(
+      final String thriftEndpoint, final String serviceName) {
+    final HttpSender sender = new HttpSender(thriftEndpoint);
+    final Process process = new Process(serviceName);
+    return new JaegerExporterHandler(sender, process);
+  }
+
+  private static SpanExporter.Handler newHandlerWithSender(
+      final HttpSender sender, final String serviceName) {
+    final Process process = new Process(serviceName);
+    return new JaegerExporterHandler(sender, process);
+  }
+
+  /**
+   * Registers the {@link JaegerTraceExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+   */
+  @VisibleForTesting
+  static void register(final SpanExporter spanExporter, final SpanExporter.Handler handler) {
+    spanExporter.registerHandler(REGISTER_NAME, handler);
+  }
+
+  /**
+   * Unregisters the {@link JaegerTraceExporter} from the OpenCensus library.
+   *
+   * @throws IllegalStateException if a Jaeger exporter is not registered.
+   * @since 0.13
+   */
+  public static void unregister() {
+    synchronized (monitor) {
+      checkState(handler != null, "Jaeger exporter is not registered.");
+      unregister(Tracing.getExportComponent().getSpanExporter());
+      handler = null;
+    }
+  }
+
+  /**
+   * Unregisters the {@link JaegerTraceExporter}.
+   *
+   * @param spanExporter the instance of the {@link SpanExporter} from where this service is
+   *     unregistered.
+   */
+  @VisibleForTesting
+  static void unregister(final SpanExporter spanExporter) {
+    spanExporter.unregisterHandler(REGISTER_NAME);
+  }
+}
diff --git a/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java
new file mode 100644
index 0000000..9d6a797
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import io.opencensus.common.Scope;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.util.Random;
+import org.junit.AfterClass;
+import org.junit.AssumptionViolatedException;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+
+public class JaegerExporterHandlerIntegrationTest {
+  private static final String JAEGER_IMAGE = "jaegertracing/all-in-one:1.3";
+  private static final int JAEGER_HTTP_PORT = 16686;
+  private static final int JAEGER_HTTP_PORT_THRIFT = 14268;
+  private static final String SERVICE_NAME = "test";
+  private static final String SPAN_NAME = "my.org/ProcessVideo";
+  private static final String START_PROCESSING_VIDEO = "Start processing video.";
+  private static final String FINISHED_PROCESSING_VIDEO = "Finished processing video.";
+
+  private static final Logger logger =
+      LoggerFactory.getLogger(JaegerExporterHandlerIntegrationTest.class);
+
+  private final HttpRequestFactory httpRequestFactory =
+      new NetHttpTransport().createRequestFactory();
+
+  private static GenericContainer<?> container;
+
+  /** Starts a docker container optionally. For example, skips if Docker is unavailable. */
+  @SuppressWarnings("rawtypes")
+  @BeforeClass
+  public static void startContainer() {
+    try {
+      container =
+          new GenericContainer(JAEGER_IMAGE)
+              .withExposedPorts(JAEGER_HTTP_PORT, JAEGER_HTTP_PORT_THRIFT)
+              .waitingFor(new HttpWaitStrategy());
+      container.start();
+    } catch (RuntimeException e) {
+      throw new AssumptionViolatedException("could not start docker container", e);
+    }
+  }
+
+  @AfterClass
+  public static void stopContainer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  @Before
+  public void before() {
+    JaegerTraceExporter.createAndRegister(thriftTracesEndpoint(), SERVICE_NAME);
+  }
+
+  @Test
+  public void exportToJaeger() throws InterruptedException, IOException {
+    Tracer tracer = Tracing.getTracer();
+    final long startTimeInMillis = currentTimeMillis();
+
+    SpanBuilder spanBuilder =
+        tracer.spanBuilder(SPAN_NAME).setRecordEvents(true).setSampler(Samplers.alwaysSample());
+    int spanDurationInMillis = new Random().nextInt(10) + 1;
+
+    Scope scopedSpan = spanBuilder.startScopedSpan();
+    try {
+      tracer.getCurrentSpan().addAnnotation(START_PROCESSING_VIDEO);
+      Thread.sleep(spanDurationInMillis); // Fake work.
+      tracer.getCurrentSpan().putAttribute("foo", AttributeValue.stringAttributeValue("bar"));
+      tracer.getCurrentSpan().addAnnotation(FINISHED_PROCESSING_VIDEO);
+    } catch (Exception e) {
+      tracer.getCurrentSpan().addAnnotation("Exception thrown when processing video.");
+      tracer.getCurrentSpan().setStatus(Status.UNKNOWN);
+      logger.error(e.getMessage());
+    } finally {
+      scopedSpan.close();
+    }
+
+    logger.info("Wait longer than the reporting duration...");
+    // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+    long timeWaitingForSpansToBeExportedInMillis = 5100L;
+    Thread.sleep(timeWaitingForSpansToBeExportedInMillis);
+    JaegerTraceExporter.unregister();
+    final long endTimeInMillis = currentTimeMillis();
+
+    // Get traces recorded by Jaeger:
+    HttpRequest request =
+        httpRequestFactory.buildGetRequest(new GenericUrl(tracesForServiceEndpoint(SERVICE_NAME)));
+    HttpResponse response = request.execute();
+    String body = response.parseAsString();
+    assertWithMessage("Response was: " + body).that(response.getStatusCode()).isEqualTo(200);
+
+    JsonObject result = new JsonParser().parse(body).getAsJsonObject();
+    // Pretty-print for debugging purposes:
+    logger.debug(new GsonBuilder().setPrettyPrinting().create().toJson(result));
+
+    assertThat(result).isNotNull();
+    assertThat(result.get("total").getAsInt()).isEqualTo(0);
+    assertThat(result.get("limit").getAsInt()).isEqualTo(0);
+    assertThat(result.get("offset").getAsInt()).isEqualTo(0);
+    assertThat(result.get("errors").getAsJsonNull()).isEqualTo(JsonNull.INSTANCE);
+    JsonArray data = result.get("data").getAsJsonArray();
+    assertThat(data).isNotNull();
+    assertThat(data.size()).isEqualTo(1);
+    JsonObject trace = data.get(0).getAsJsonObject();
+    assertThat(trace).isNotNull();
+    assertThat(trace.get("traceID").getAsString()).matches("[a-z0-9]{1,32}");
+
+    JsonArray spans = trace.get("spans").getAsJsonArray();
+    assertThat(spans).isNotNull();
+    assertThat(spans.size()).isEqualTo(1);
+
+    JsonObject span = spans.get(0).getAsJsonObject();
+    assertThat(span).isNotNull();
+    assertThat(span.get("traceID").getAsString()).matches("[a-z0-9]{1,32}");
+    assertThat(span.get("spanID").getAsString()).matches("[a-z0-9]{1,16}");
+    assertThat(span.get("flags").getAsInt()).isEqualTo(1);
+    assertThat(span.get("operationName").getAsString()).isEqualTo(SPAN_NAME);
+    assertThat(span.get("references").getAsJsonArray()).isEmpty();
+    assertThat(span.get("startTime").getAsLong())
+        .isAtLeast(MILLISECONDS.toMicros(startTimeInMillis));
+    assertThat(span.get("startTime").getAsLong()).isAtMost(MILLISECONDS.toMicros(endTimeInMillis));
+    assertThat(span.get("duration").getAsLong())
+        .isAtLeast(MILLISECONDS.toMicros(spanDurationInMillis));
+    assertThat(span.get("duration").getAsLong())
+        .isAtMost(
+            MILLISECONDS.toMicros(spanDurationInMillis + timeWaitingForSpansToBeExportedInMillis));
+
+    JsonArray tags = span.get("tags").getAsJsonArray();
+    assertThat(tags.size()).isEqualTo(1);
+    JsonObject tag = tags.get(0).getAsJsonObject();
+    assertThat(tag.get("key").getAsString()).isEqualTo("foo");
+    assertThat(tag.get("type").getAsString()).isEqualTo("string");
+    assertThat(tag.get("value").getAsString()).isEqualTo("bar");
+
+    JsonArray logs = span.get("logs").getAsJsonArray();
+    assertThat(logs.size()).isEqualTo(2);
+
+    JsonObject log1 = logs.get(0).getAsJsonObject();
+    long ts1 = log1.get("timestamp").getAsLong();
+    assertThat(ts1).isAtLeast(MILLISECONDS.toMicros(startTimeInMillis));
+    assertThat(ts1).isAtMost(MILLISECONDS.toMicros(endTimeInMillis));
+    JsonArray fields1 = log1.get("fields").getAsJsonArray();
+    assertThat(fields1.size()).isEqualTo(1);
+    JsonObject field1 = fields1.get(0).getAsJsonObject();
+    assertThat(field1.get("key").getAsString()).isEqualTo("description");
+    assertThat(field1.get("type").getAsString()).isEqualTo("string");
+    assertThat(field1.get("value").getAsString()).isEqualTo(START_PROCESSING_VIDEO);
+
+    JsonObject log2 = logs.get(1).getAsJsonObject();
+    long ts2 = log2.get("timestamp").getAsLong();
+    assertThat(ts2).isAtLeast(MILLISECONDS.toMicros(startTimeInMillis));
+    assertThat(ts2).isAtMost(MILLISECONDS.toMicros(endTimeInMillis));
+    assertThat(ts2).isAtLeast(ts1);
+    JsonArray fields2 = log2.get("fields").getAsJsonArray();
+    assertThat(fields2.size()).isEqualTo(1);
+    JsonObject field2 = fields2.get(0).getAsJsonObject();
+    assertThat(field2.get("key").getAsString()).isEqualTo("description");
+    assertThat(field2.get("type").getAsString()).isEqualTo("string");
+    assertThat(field2.get("value").getAsString()).isEqualTo(FINISHED_PROCESSING_VIDEO);
+
+    assertThat(span.get("processID").getAsString()).isEqualTo("p1");
+    assertThat(span.get("warnings").getAsJsonNull()).isEqualTo(JsonNull.INSTANCE);
+
+    JsonObject processes = trace.get("processes").getAsJsonObject();
+    assertThat(processes.size()).isEqualTo(1);
+    JsonObject p1 = processes.get("p1").getAsJsonObject();
+    assertThat(p1.get("serviceName").getAsString()).isEqualTo(SERVICE_NAME);
+    assertThat(p1.get("tags").getAsJsonArray().size()).isEqualTo(0);
+    assertThat(trace.get("warnings").getAsJsonNull()).isEqualTo(JsonNull.INSTANCE);
+  }
+
+  private static String thriftTracesEndpoint() {
+    return format(
+        "http://%s:%s/api/traces",
+        container.getContainerIpAddress(), container.getMappedPort(JAEGER_HTTP_PORT_THRIFT));
+  }
+
+  private static String tracesForServiceEndpoint(String service) {
+    return format(
+        "http://%s:%s/api/traces?service=%s",
+        container.getContainerIpAddress(), container.getMappedPort(JAEGER_HTTP_PORT), service);
+  }
+}
diff --git a/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java
new file mode 100644
index 0000000..f918f01
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Collections.singletonList;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.uber.jaeger.exceptions.SenderException;
+import com.uber.jaeger.senders.HttpSender;
+import com.uber.jaeger.thriftjava.Log;
+import com.uber.jaeger.thriftjava.Process;
+import com.uber.jaeger.thriftjava.Span;
+import com.uber.jaeger.thriftjava.SpanRef;
+import com.uber.jaeger.thriftjava.SpanRefType;
+import com.uber.jaeger.thriftjava.Tag;
+import com.uber.jaeger.thriftjava.TagType;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class JaegerExporterHandlerTest {
+  private static final byte FF = (byte) 0xFF;
+
+  private final HttpSender mockSender = mock(HttpSender.class);
+  private final Process process = new Process("test");
+  private final JaegerExporterHandler handler = new JaegerExporterHandler(mockSender, process);
+
+  @Captor private ArgumentCaptor<List<Span>> captor;
+
+  @Test
+  public void exportShouldConvertFromSpanDataToJaegerThriftSpan() throws SenderException {
+    final long startTime = 1519629870001L;
+    final long endTime = 1519630148002L;
+    final SpanData spanData =
+        SpanData.create(
+            sampleSpanContext(),
+            SpanId.fromBytes(new byte[] {(byte) 0x7F, FF, FF, FF, FF, FF, FF, FF}),
+            true,
+            "test",
+            Timestamp.fromMillis(startTime),
+            SpanData.Attributes.create(sampleAttributes(), 0),
+            SpanData.TimedEvents.create(singletonList(sampleAnnotation()), 0),
+            SpanData.TimedEvents.create(singletonList(sampleMessageEvent()), 0),
+            SpanData.Links.create(sampleLinks(), 0),
+            0,
+            Status.OK,
+            Timestamp.fromMillis(endTime));
+
+    handler.export(singletonList(spanData));
+
+    verify(mockSender).send(eq(process), captor.capture());
+    List<Span> spans = captor.getValue();
+
+    assertThat(spans.size()).isEqualTo(1);
+    Span span = spans.get(0);
+
+    assertThat(span.operationName).isEqualTo("test");
+    assertThat(span.spanId).isEqualTo(256L);
+    assertThat(span.traceIdHigh).isEqualTo(-72057594037927936L);
+    assertThat(span.traceIdLow).isEqualTo(1L);
+    assertThat(span.parentSpanId).isEqualTo(Long.MAX_VALUE);
+    assertThat(span.flags).isEqualTo(1);
+    assertThat(span.startTime).isEqualTo(MILLISECONDS.toMicros(startTime));
+    assertThat(span.duration).isEqualTo(MILLISECONDS.toMicros(endTime - startTime));
+
+    assertThat(span.tags.size()).isEqualTo(3);
+    assertThat(span.tags)
+        .containsExactly(
+            new Tag("BOOL", TagType.BOOL).setVBool(false),
+            new Tag("LONG", TagType.LONG).setVLong(Long.MAX_VALUE),
+            new Tag("STRING", TagType.STRING)
+                .setVStr(
+                    "Judge of a man by his questions rather than by his answers. -- Voltaire"));
+
+    assertThat(span.logs.size()).isEqualTo(1);
+    Log log = span.logs.get(0);
+    assertThat(log.timestamp).isEqualTo(1519629872987654L);
+    assertThat(log.fields.size()).isEqualTo(4);
+    assertThat(log.fields)
+        .containsExactly(
+            new Tag("description", TagType.STRING).setVStr("annotation #1"),
+            new Tag("bool", TagType.BOOL).setVBool(true),
+            new Tag("long", TagType.LONG).setVLong(1337L),
+            new Tag("string", TagType.STRING)
+                .setVStr("Kind words do not cost much. Yet they accomplish much. -- Pascal"));
+
+    assertThat(span.references.size()).isEqualTo(1);
+    SpanRef reference = span.references.get(0);
+    assertThat(reference.traceIdHigh).isEqualTo(-1L);
+    assertThat(reference.traceIdLow).isEqualTo(-256L);
+    assertThat(reference.spanId).isEqualTo(512L);
+    assertThat(reference.refType).isEqualTo(SpanRefType.CHILD_OF);
+  }
+
+  private static SpanContext sampleSpanContext() {
+    return SpanContext.create(
+        TraceId.fromBytes(new byte[] {FF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}),
+        SpanId.fromBytes(new byte[] {0, 0, 0, 0, 0, 0, 1, 0}),
+        TraceOptions.builder().setIsSampled(true).build());
+  }
+
+  private static ImmutableMap<String, AttributeValue> sampleAttributes() {
+    return ImmutableMap.of(
+        "BOOL", AttributeValue.booleanAttributeValue(false),
+        "LONG", AttributeValue.longAttributeValue(Long.MAX_VALUE),
+        "STRING",
+            AttributeValue.stringAttributeValue(
+                "Judge of a man by his questions rather than by his answers. -- Voltaire"));
+  }
+
+  private static SpanData.TimedEvent<Annotation> sampleAnnotation() {
+    return SpanData.TimedEvent.create(
+        Timestamp.create(1519629872L, 987654321),
+        Annotation.fromDescriptionAndAttributes(
+            "annotation #1",
+            ImmutableMap.of(
+                "bool", AttributeValue.booleanAttributeValue(true),
+                "long", AttributeValue.longAttributeValue(1337L),
+                "string",
+                    AttributeValue.stringAttributeValue(
+                        "Kind words do not cost much. Yet they accomplish much. -- Pascal"))));
+  }
+
+  private static SpanData.TimedEvent<MessageEvent> sampleMessageEvent() {
+    return SpanData.TimedEvent.create(
+        Timestamp.create(1519629871L, 123456789),
+        MessageEvent.builder(MessageEvent.Type.SENT, 42L).build());
+  }
+
+  private static List<Link> sampleLinks() {
+    return Lists.newArrayList(
+        Link.fromSpanContext(
+            SpanContext.create(
+                TraceId.fromBytes(
+                    new byte[] {FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, 0}),
+                SpanId.fromBytes(new byte[] {0, 0, 0, 0, 0, 0, 2, 0}),
+                TraceOptions.builder().setIsSampled(false).build()),
+            Link.Type.CHILD_LINKED_SPAN,
+            ImmutableMap.of(
+                "Bool", AttributeValue.booleanAttributeValue(true),
+                "Long", AttributeValue.longAttributeValue(299792458L),
+                "String",
+                    AttributeValue.stringAttributeValue(
+                        "Man is condemned to be free; because once thrown into the world, "
+                            + "he is responsible for everything he does. -- Sartre"))));
+  }
+}
diff --git a/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java
new file mode 100644
index 0000000..c00b013
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class JaegerTraceExporterTest {
+  @Mock private SpanExporter spanExporter;
+
+  @Mock private SpanExporter.Handler handler;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void registerUnregisterJaegerExporter() {
+    JaegerTraceExporter.register(spanExporter, handler);
+    verify(spanExporter)
+        .registerHandler(
+            eq("io.opencensus.exporter.trace.jaeger.JaegerTraceExporter"), same(handler));
+    JaegerTraceExporter.unregister(spanExporter);
+    verify(spanExporter)
+        .unregisterHandler(eq("io.opencensus.exporter.trace.jaeger.JaegerTraceExporter"));
+  }
+}
diff --git a/exporters/trace/logging/README.md b/exporters/trace/logging/README.md
new file mode 100644
index 0000000..51f2566
--- /dev/null
+++ b/exporters/trace/logging/README.md
@@ -0,0 +1,57 @@
+# OpenCensus Logging Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Logging trace exporter* is a trace exporter that logs all data to the system log.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-exporter-trace-logging</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-logging:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+### Register the exporter
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) throws Exception {
+    LoggingTraceExporter.register();
+    // ...
+  }
+}
+```
+
+[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-exporter-trace-logging/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-logging
diff --git a/exporters/trace/logging/build.gradle b/exporters/trace/logging/build.gradle
new file mode 100644
index 0000000..a7fb0ff
--- /dev/null
+++ b/exporters/trace/logging/build.gradle
@@ -0,0 +1,11 @@
+description = 'OpenCensus Trace Logging Exporter'
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.guava
+
+    testCompile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
\ No newline at end of file
diff --git a/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java
new file mode 100644
index 0000000..46f01ff
--- /dev/null
+++ b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java
@@ -0,0 +1,81 @@
+/*
+ * 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.exporter.trace.logging;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.export.SpanExporter;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An OpenCensus span exporter implementation which logs all data.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ *   LoggingExporter.register();
+ *   ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @deprecated Deprecated due to inconsistent naming. Use {@link LoggingTraceExporter}.
+ * @since 0.6
+ */
+@ThreadSafe
+@Deprecated
+public final class LoggingExporter {
+  private LoggingExporter() {}
+
+  /**
+   * Registers the Logging exporter to the OpenCensus library.
+   *
+   * @since 0.6
+   */
+  public static void register() {
+    LoggingTraceExporter.register();
+  }
+
+  /**
+   * Registers the {@code LoggingHandler}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+   */
+  @VisibleForTesting
+  static void register(SpanExporter spanExporter) {
+    LoggingTraceExporter.register(spanExporter);
+  }
+
+  /**
+   * Unregisters the Logging exporter from the OpenCensus library.
+   *
+   * @since 0.6
+   */
+  public static void unregister() {
+    LoggingTraceExporter.unregister();
+  }
+
+  /**
+   * Unregisters the {@code LoggingHandler}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+   *     unregistered.
+   */
+  @VisibleForTesting
+  static void unregister(SpanExporter spanExporter) {
+    LoggingTraceExporter.unregister(spanExporter);
+  }
+}
diff --git a/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java
new file mode 100644
index 0000000..9267e20
--- /dev/null
+++ b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java
@@ -0,0 +1,101 @@
+/*
+ * 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.exporter.trace.logging;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An OpenCensus span exporter implementation which logs all data.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ *   LoggingTraceExporter.register();
+ *   ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+@ThreadSafe
+public final class LoggingTraceExporter {
+  private static final Logger logger = Logger.getLogger(LoggingTraceExporter.class.getName());
+  private static final String REGISTER_NAME = LoggingTraceExporter.class.getName();
+  private static final LoggingExporterHandler HANDLER = new LoggingExporterHandler();
+
+  private LoggingTraceExporter() {}
+
+  /**
+   * Registers the Logging exporter to the OpenCensus library.
+   *
+   * @since 0.12
+   */
+  public static void register() {
+    register(Tracing.getExportComponent().getSpanExporter());
+  }
+
+  /**
+   * Registers the {@code LoggingHandler}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+   */
+  @VisibleForTesting
+  static void register(SpanExporter spanExporter) {
+    spanExporter.registerHandler(REGISTER_NAME, HANDLER);
+  }
+
+  /**
+   * Unregisters the Logging exporter from the OpenCensus library.
+   *
+   * @since 0.12
+   */
+  public static void unregister() {
+    unregister(Tracing.getExportComponent().getSpanExporter());
+  }
+
+  /**
+   * Unregisters the {@code LoggingHandler}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+   *     unregistered.
+   */
+  @VisibleForTesting
+  static void unregister(SpanExporter spanExporter) {
+    spanExporter.unregisterHandler(REGISTER_NAME);
+  }
+
+  @VisibleForTesting
+  static final class LoggingExporterHandler extends Handler {
+    @Override
+    public void export(Collection<SpanData> spanDataList) {
+      // TODO(bdrutu): Use JSON as a standard format for logging SpanData and define this to be
+      // compatible between languages.
+      for (SpanData spanData : spanDataList) {
+        logger.log(Level.INFO, spanData.toString());
+      }
+    }
+  }
+}
diff --git a/exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java b/exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java
new file mode 100644
index 0000000..c2b77e4
--- /dev/null
+++ b/exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.exporter.trace.logging;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter.LoggingExporterHandler;
+import io.opencensus.trace.export.SpanExporter;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link LoggingTraceExporter}. */
+@RunWith(JUnit4.class)
+public class LoggingTraceExporterTest {
+  @Mock private SpanExporter spanExporter;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void registerUnregisterLoggingService() {
+    LoggingTraceExporter.register(spanExporter);
+    verify(spanExporter)
+        .registerHandler(
+            eq("io.opencensus.exporter.trace.logging.LoggingTraceExporter"),
+            any(LoggingExporterHandler.class));
+    LoggingTraceExporter.unregister(spanExporter);
+    verify(spanExporter)
+        .unregisterHandler(eq("io.opencensus.exporter.trace.logging.LoggingTraceExporter"));
+  }
+}
diff --git a/exporters/trace/ocagent/README.md b/exporters/trace/ocagent/README.md
new file mode 100644
index 0000000..4f25bd6
--- /dev/null
+++ b/exporters/trace/ocagent/README.md
@@ -0,0 +1,48 @@
+# OpenCensus Java OC-Agent Trace Exporter
+
+The *OpenCensus Java OC-Agent Trace Exporter* is the Java implementation of the OpenCensus Agent
+(OC-Agent) Trace Exporter.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.17.0</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-exporter-trace-ocagent</artifactId>
+    <version>0.17.0</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.17.0</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.17.0'
+compile 'io.opencensus:opencensus-exporter-trace-ocagent:0.17.0'
+runtime 'io.opencensus:opencensus-impl:0.17.0'
+```
+
+### Register the exporter
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) throws Exception {
+    OcAgentTraceExporter.createAndRegister();
+    // ...
+  }
+}
+```
diff --git a/exporters/trace/ocagent/build.gradle b/exporters/trace/ocagent/build.gradle
new file mode 100644
index 0000000..777c08d
--- /dev/null
+++ b/exporters/trace/ocagent/build.gradle
@@ -0,0 +1,21 @@
+description = 'OpenCensus Java OC-Agent Trace Exporter'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.7
+    it.targetCompatibility = 1.7
+}
+
+dependencies {
+    compileOnly libraries.auto_value
+
+    compile project(':opencensus-api'),
+            project(':opencensus-contrib-monitored-resource-util'),
+            libraries.grpc_core,
+            libraries.grpc_netty,
+            libraries.grpc_stub,
+            libraries.opencensus_proto
+
+    testCompile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java
new file mode 100644
index 0000000..6572980
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.OpenCensusLibraryInformation;
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResourceUtils;
+import io.opencensus.proto.agent.common.v1.LibraryInfo;
+import io.opencensus.proto.agent.common.v1.LibraryInfo.Language;
+import io.opencensus.proto.agent.common.v1.Node;
+import io.opencensus.proto.agent.common.v1.ProcessIdentifier;
+import io.opencensus.proto.agent.common.v1.ServiceInfo;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** Utilities for detecting and creating {@link Node}. */
+final class OcAgentNodeUtils {
+
+  // The current version of the OpenCensus OC-Agent Exporter.
+  @VisibleForTesting
+  static final String OC_AGENT_EXPORTER_VERSION = "0.17.0-SNAPSHOT"; // CURRENT_OPENCENSUS_VERSION
+
+  @VisibleForTesting static final String RESOURCE_TYPE_ATTRIBUTE_KEY = "OPENCENSUS_SOURCE_TYPE";
+  @VisibleForTesting static final String RESOURCE_LABEL_ATTRIBUTE_KEY = "OPENCENSUS_SOURCE_LABELS";
+
+  @Nullable
+  private static final MonitoredResource RESOURCE = MonitoredResourceUtils.getDefaultResource();
+
+  // Creates a Node with information from the OpenCensus library and environment variables.
+  static Node getNodeInfo(String serviceName) {
+    String jvmName = ManagementFactory.getRuntimeMXBean().getName();
+    Timestamp censusTimestamp = Timestamp.fromMillis(System.currentTimeMillis());
+    return Node.newBuilder()
+        .setIdentifier(getProcessIdentifier(jvmName, censusTimestamp))
+        .setLibraryInfo(getLibraryInfo(OpenCensusLibraryInformation.VERSION))
+        .setServiceInfo(getServiceInfo(serviceName))
+        .putAllAttributes(getAttributeMap(RESOURCE))
+        .build();
+  }
+
+  // Creates process identifier with the given JVM name and start time.
+  @VisibleForTesting
+  static ProcessIdentifier getProcessIdentifier(String jvmName, Timestamp censusTimestamp) {
+    String hostname;
+    int pid;
+    // jvmName should be something like '<pid>@<hostname>', at least in Oracle and OpenJdk JVMs
+    int delimiterIndex = jvmName.indexOf('@');
+    if (delimiterIndex < 1) {
+      // Not the expected format, generate a random number.
+      try {
+        hostname = InetAddress.getLocalHost().getHostName();
+      } catch (UnknownHostException e) {
+        hostname = "localhost";
+      }
+      // Generate a random number as the PID.
+      pid = new SecureRandom().nextInt();
+    } else {
+      hostname = jvmName.substring(delimiterIndex + 1, jvmName.length());
+      try {
+        pid = Integer.parseInt(jvmName.substring(0, delimiterIndex));
+      } catch (NumberFormatException e) {
+        // Generate a random number as the PID if format is unexpected.
+        pid = new SecureRandom().nextInt();
+      }
+    }
+
+    return ProcessIdentifier.newBuilder()
+        .setHostName(hostname)
+        .setPid(pid)
+        .setStartTimestamp(TraceProtoUtils.toTimestampProto(censusTimestamp))
+        .build();
+  }
+
+  // Creates library info with the given OpenCensus Java version.
+  @VisibleForTesting
+  static LibraryInfo getLibraryInfo(String currentOcJavaVersion) {
+    return LibraryInfo.newBuilder()
+        .setLanguage(Language.JAVA)
+        .setCoreLibraryVersion(currentOcJavaVersion)
+        .setExporterVersion(OC_AGENT_EXPORTER_VERSION)
+        .build();
+  }
+
+  // Creates service info with the given service name.
+  @VisibleForTesting
+  static ServiceInfo getServiceInfo(String serviceName) {
+    return ServiceInfo.newBuilder().setName(serviceName).build();
+  }
+
+  /*
+   * Creates an attribute map with the given MonitoredResource.
+   * If the given resource is not null, the attribute map contains exactly two entries:
+   *
+   * OPENCENSUS_SOURCE_TYPE:
+   *   A string that describes the type of the resource prefixed by a domain namespace,
+   *   e.g. “kubernetes.io/container”.
+   * OPENCENSUS_SOURCE_LABELS:
+   *   A comma-separated list of labels describing the source in more detail,
+   *   e.g. “key1=val1,key2=val2”. The allowed character set is appropriately constrained.
+   */
+  // TODO: update the resource attributes once we have an agreement on the resource specs:
+  // https://github.com/census-instrumentation/opencensus-specs/pull/162.
+  @VisibleForTesting
+  static Map<String, String> getAttributeMap(@Nullable MonitoredResource resource) {
+    if (resource == null) {
+      return Collections.emptyMap();
+    } else {
+      Map<String, String> resourceAttributes = new HashMap<String, String>();
+      resourceAttributes.put(RESOURCE_TYPE_ATTRIBUTE_KEY, resource.getResourceType().name());
+      resourceAttributes.put(RESOURCE_LABEL_ATTRIBUTE_KEY, getConcatenatedResourceLabels(resource));
+      return resourceAttributes;
+    }
+  }
+
+  // Encodes the attributes of MonitoredResource into a comma-separated list of labels.
+  // For example "aws_account=account1,instance_id=instance1,region=us-east-2".
+  private static String getConcatenatedResourceLabels(MonitoredResource resource) {
+    StringBuilder resourceLabels = new StringBuilder();
+    if (resource instanceof AwsEc2InstanceMonitoredResource) {
+      AwsEc2InstanceMonitoredResource awsEc2Resource = (AwsEc2InstanceMonitoredResource) resource;
+      putIntoBuilderIfHasValue(resourceLabels, "aws_account", awsEc2Resource.getAccount());
+      putIntoBuilderIfHasValue(resourceLabels, "instance_id", awsEc2Resource.getInstanceId());
+      putIntoBuilderIfHasValue(resourceLabels, "region", awsEc2Resource.getRegion());
+    } else if (resource instanceof GcpGceInstanceMonitoredResource) {
+      GcpGceInstanceMonitoredResource gceResource = (GcpGceInstanceMonitoredResource) resource;
+      putIntoBuilderIfHasValue(resourceLabels, "gcp_account", gceResource.getAccount());
+      putIntoBuilderIfHasValue(resourceLabels, "instance_id", gceResource.getInstanceId());
+      putIntoBuilderIfHasValue(resourceLabels, "zone", gceResource.getZone());
+    } else if (resource instanceof GcpGkeContainerMonitoredResource) {
+      GcpGkeContainerMonitoredResource gkeResource = (GcpGkeContainerMonitoredResource) resource;
+      putIntoBuilderIfHasValue(resourceLabels, "gcp_account", gkeResource.getAccount());
+      putIntoBuilderIfHasValue(resourceLabels, "instance_id", gkeResource.getInstanceId());
+      putIntoBuilderIfHasValue(resourceLabels, "location", gkeResource.getZone());
+      putIntoBuilderIfHasValue(resourceLabels, "namespace_name", gkeResource.getNamespaceId());
+      putIntoBuilderIfHasValue(resourceLabels, "cluster_name", gkeResource.getClusterName());
+      putIntoBuilderIfHasValue(resourceLabels, "container_name", gkeResource.getContainerName());
+      putIntoBuilderIfHasValue(resourceLabels, "pod_name", gkeResource.getPodId());
+    }
+    return resourceLabels.toString();
+  }
+
+  // If the given resourceValue is not empty, encodes resourceKey and resourceValue as
+  // "resourceKey:resourceValue" and puts it into the given StringBuilder. Otherwise skip the value.
+  private static void putIntoBuilderIfHasValue(
+      StringBuilder builder, String resourceKey, String resourceValue) {
+    if (resourceValue.isEmpty()) {
+      return;
+    }
+    if (!(builder.length() == 0)) {
+      // Appends the comma separator to the front, if the StringBuilder already has entries.
+      builder.append(',');
+    }
+    builder.append(resourceKey);
+    builder.append('=');
+    builder.append(resourceValue);
+  }
+
+  private OcAgentNodeUtils() {}
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java
new file mode 100644
index 0000000..5c468de
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * The implementation of the OpenCensus Agent (OC-Agent) Trace Exporter.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ *   OcAgentTraceExporter.createAndRegister();
+ *   ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public final class OcAgentTraceExporter {
+
+  private static final Object monitor = new Object();
+  private static final String REGISTER_NAME = OcAgentTraceExporter.class.getName();
+
+  @GuardedBy("monitor")
+  @Nullable
+  private static Handler handler = null;
+
+  private OcAgentTraceExporter() {}
+
+  /**
+   * Creates a {@code OcAgentTraceExporterHandler} with default configurations and registers it to
+   * the OpenCensus library.
+   *
+   * @since 0.17
+   */
+  public static void createAndRegister() {
+    synchronized (monitor) {
+      checkState(handler == null, "OC-Agent exporter is already registered.");
+      OcAgentTraceExporterHandler newHandler = new OcAgentTraceExporterHandler();
+      registerInternal(newHandler);
+    }
+  }
+
+  /**
+   * Creates a {@code OcAgentTraceExporterHandler} with the given configurations and registers it to
+   * the OpenCensus library.
+   *
+   * @param configuration the {@code OcAgentTraceExporterConfiguration}.
+   * @since 0.17
+   */
+  public static void createAndRegister(OcAgentTraceExporterConfiguration configuration) {
+    synchronized (monitor) {
+      checkState(handler == null, "OC-Agent exporter is already registered.");
+      OcAgentTraceExporterHandler newHandler =
+          new OcAgentTraceExporterHandler(
+              configuration.getEndPoint(),
+              configuration.getServiceName(),
+              configuration.getUseInsecure(),
+              configuration.getRetryInterval(),
+              configuration.getEnableConfig());
+      registerInternal(newHandler);
+    }
+  }
+
+  /**
+   * Registers the {@code OcAgentTraceExporterHandler}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+   */
+  @VisibleForTesting
+  static void register(SpanExporter spanExporter, Handler handler) {
+    spanExporter.registerHandler(REGISTER_NAME, handler);
+  }
+
+  private static void registerInternal(Handler newHandler) {
+    synchronized (monitor) {
+      handler = newHandler;
+      register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+    }
+  }
+
+  /**
+   * Unregisters the OC-Agent exporter from the OpenCensus library.
+   *
+   * @since 0.17
+   */
+  public static void unregister() {
+    unregister(Tracing.getExportComponent().getSpanExporter());
+  }
+
+  /**
+   * Unregisters the {@code OcAgentTraceExporterHandler}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+   *     unregistered.
+   */
+  @VisibleForTesting
+  static void unregister(SpanExporter spanExporter) {
+    spanExporter.unregisterHandler(REGISTER_NAME);
+  }
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java
new file mode 100644
index 0000000..c7bf1e9
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link OcAgentTraceExporter}.
+ *
+ * @since 0.17
+ */
+@AutoValue
+@Immutable
+public abstract class OcAgentTraceExporterConfiguration {
+
+  OcAgentTraceExporterConfiguration() {}
+
+  /**
+   * Returns the end point of OC-Agent. The end point can be dns, ip:port, etc.
+   *
+   * @return the end point of OC-Agent.
+   * @since 0.17
+   */
+  @Nullable
+  public abstract String getEndPoint();
+
+  /**
+   * Returns whether to disable client transport security for the exporter's gRPC connection or not.
+   *
+   * @return whether to disable client transport security for the exporter's gRPC connection or not.
+   * @since 0.17
+   */
+  @Nullable
+  public abstract Boolean getUseInsecure();
+
+  /**
+   * Returns the service name to be used for this {@link OcAgentTraceExporter}.
+   *
+   * @return the service name.
+   * @since 0.17
+   */
+  @Nullable
+  public abstract String getServiceName();
+
+  /**
+   * Returns the retry time interval when trying to connect to Agent.
+   *
+   * @return the retry time interval.
+   * @since 0.17
+   */
+  @Nullable
+  public abstract Duration getRetryInterval();
+
+  /**
+   * Returns whether the {@link OcAgentTraceExporter} should handle the config streams.
+   *
+   * @return whether the {@code OcAgentTraceExporter} should handle the config streams.
+   * @since 0.17
+   */
+  public abstract boolean getEnableConfig();
+
+  /**
+   * Returns a new {@link Builder}.
+   *
+   * @return a {@code Builder}.
+   * @since 0.17
+   */
+  public static Builder builder() {
+    return new AutoValue_OcAgentTraceExporterConfiguration.Builder().setEnableConfig(true);
+  }
+
+  /**
+   * Builder for {@link OcAgentTraceExporterConfiguration}.
+   *
+   * @since 0.17
+   */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    Builder() {}
+
+    /**
+     * Sets the end point of OC-Agent server.
+     *
+     * @param endPoint the end point of OC-Agent.
+     * @return this.
+     * @since 0.17
+     */
+    public abstract Builder setEndPoint(String endPoint);
+
+    /**
+     * Sets whether to disable client transport security for the exporter's gRPC connection or not.
+     *
+     * @param useInsecure whether disable client transport security for the exporter's gRPC
+     *     connection.
+     * @return this.
+     * @since 0.17
+     */
+    public abstract Builder setUseInsecure(Boolean useInsecure);
+
+    /**
+     * Sets the service name to be used for this {@link OcAgentTraceExporter}.
+     *
+     * @param serviceName the service name.
+     * @return this.
+     * @since 0.17
+     */
+    public abstract Builder setServiceName(String serviceName);
+
+    /**
+     * Sets the retry time interval when trying to connect to Agent.
+     *
+     * @param retryInterval the retry time interval.
+     * @return this.
+     * @since 0.17
+     */
+    public abstract Builder setRetryInterval(Duration retryInterval);
+
+    /**
+     * Sets whether {@link OcAgentTraceExporter} should handle the config streams.
+     *
+     * @param enableConfig whether {@code OcAgentTraceExporter} should handle the config streams.
+     * @return this.
+     * @since 0.17
+     */
+    public abstract Builder setEnableConfig(boolean enableConfig);
+
+    // TODO(songya): add an option that controls whether to always keep the RPC connection alive.
+
+    /**
+     * Builds a {@link OcAgentTraceExporterConfiguration}.
+     *
+     * @return a {@code OcAgentTraceExporterConfiguration}.
+     * @since 0.17
+     */
+    public abstract OcAgentTraceExporterConfiguration build();
+  }
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java
new file mode 100644
index 0000000..5edc06d
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import io.opencensus.common.Duration;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.util.Collection;
+import javax.annotation.Nullable;
+
+/** Exporting handler for OC-Agent Tracing. */
+final class OcAgentTraceExporterHandler extends Handler {
+
+  private static final String DEFAULT_END_POINT = "localhost:55678";
+  private static final String DEFAULT_SERVICE_NAME = "OpenCensus";
+  private static final Duration DEFAULT_RETRY_INTERVAL = Duration.create(300, 0); // 5 minutes
+
+  OcAgentTraceExporterHandler() {
+    this(null, null, null, null, /* enableConfig= */ true);
+  }
+
+  OcAgentTraceExporterHandler(
+      @Nullable String endPoint,
+      @Nullable String serviceName,
+      @Nullable Boolean useInsecure,
+      @Nullable Duration retryInterval,
+      boolean enableConfig) {
+    // if (endPoint == null) {
+    //   endPoint = DEFAULT_END_POINT;
+    // }
+    // if (serviceName == null) {
+    //   serviceName = DEFAULT_SERVICE_NAME;
+    // }
+    // if (useInsecure == null) {
+    //   useInsecure = false;
+    // }
+    // if (retryInterval == null) {
+    //   retryInterval = DEFAULT_RETRY_INTERVAL;
+    // }
+    // OcAgentTraceServiceClients.startAttemptsToConnectToAgent(
+    //     endPoint, useInsecure, serviceName, retryInterval.toMillis(), enableConfig);
+  }
+
+  @Override
+  public void export(Collection<SpanData> spanDataList) {
+    // OcAgentTraceServiceClients.onExport(spanDataList);
+  }
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java
new file mode 100644
index 0000000..ec778ba
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.UInt32Value;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.AttributeValue;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.ProbabilitySampler;
+import io.opencensus.proto.trace.v1.Span;
+import io.opencensus.proto.trace.v1.Span.Attributes;
+import io.opencensus.proto.trace.v1.Span.Link;
+import io.opencensus.proto.trace.v1.Span.Links;
+import io.opencensus.proto.trace.v1.Span.SpanKind;
+import io.opencensus.proto.trace.v1.Span.TimeEvent;
+import io.opencensus.proto.trace.v1.Span.TimeEvent.MessageEvent;
+import io.opencensus.proto.trace.v1.Span.Tracestate;
+import io.opencensus.proto.trace.v1.Span.Tracestate.Entry;
+import io.opencensus.proto.trace.v1.Status;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import io.opencensus.proto.trace.v1.TruncatableString;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Utilities for converting the Tracing data models in OpenCensus Java to/from OpenCensus Proto. */
+final class TraceProtoUtils {
+
+  // Constant functions for AttributeValue.
+  private static final Function<String, /*@Nullable*/ AttributeValue> stringAttributeValueFunction =
+      new Function<String, /*@Nullable*/ AttributeValue>() {
+        @Override
+        public AttributeValue apply(String stringValue) {
+          return AttributeValue.newBuilder()
+              .setStringValue(toTruncatableStringProto(stringValue))
+              .build();
+        }
+      };
+
+  private static final Function<Boolean, /*@Nullable*/ AttributeValue>
+      booleanAttributeValueFunction =
+          new Function<Boolean, /*@Nullable*/ AttributeValue>() {
+            @Override
+            public AttributeValue apply(Boolean booleanValue) {
+              return AttributeValue.newBuilder().setBoolValue(booleanValue).build();
+            }
+          };
+
+  private static final Function<Long, /*@Nullable*/ AttributeValue> longAttributeValueFunction =
+      new Function<Long, /*@Nullable*/ AttributeValue>() {
+        @Override
+        public AttributeValue apply(Long longValue) {
+          return AttributeValue.newBuilder().setIntValue(longValue).build();
+        }
+      };
+
+  private static final Function<Double, /*@Nullable*/ AttributeValue> doubleAttributeValueFunction =
+      new Function<Double, /*@Nullable*/ AttributeValue>() {
+        @Override
+        public AttributeValue apply(Double doubleValue) {
+          return AttributeValue.newBuilder().setDoubleValue(doubleValue).build();
+        }
+      };
+
+  /**
+   * Converts {@link SpanData} to {@link Span} proto.
+   *
+   * @param spanData the {@code SpanData}.
+   * @return proto representation of {@code Span}.
+   */
+  static Span toSpanProto(SpanData spanData) {
+    SpanContext spanContext = spanData.getContext();
+    TraceId traceId = spanContext.getTraceId();
+    SpanId spanId = spanContext.getSpanId();
+    Span.Builder spanBuilder =
+        Span.newBuilder()
+            .setTraceId(toByteString(traceId.getBytes()))
+            .setSpanId(toByteString(spanId.getBytes()))
+            .setTracestate(toTracestateProto(spanContext.getTracestate()))
+            .setName(toTruncatableStringProto(spanData.getName()))
+            .setStartTime(toTimestampProto(spanData.getStartTimestamp()))
+            .setAttributes(toAttributesProto(spanData.getAttributes()))
+            .setTimeEvents(
+                toTimeEventsProto(spanData.getAnnotations(), spanData.getMessageEvents()))
+            .setLinks(toLinksProto(spanData.getLinks()));
+
+    Kind kind = spanData.getKind();
+    if (kind != null) {
+      spanBuilder.setKind(toSpanKindProto(kind));
+    }
+
+    io.opencensus.trace.Status status = spanData.getStatus();
+    if (status != null) {
+      spanBuilder.setStatus(toStatusProto(status));
+    }
+
+    Timestamp end = spanData.getEndTimestamp();
+    if (end != null) {
+      spanBuilder.setEndTime(toTimestampProto(end));
+    }
+
+    Integer childSpanCount = spanData.getChildSpanCount();
+    if (childSpanCount != null) {
+      spanBuilder.setChildSpanCount(UInt32Value.newBuilder().setValue(childSpanCount).build());
+    }
+
+    Boolean hasRemoteParent = spanData.getHasRemoteParent();
+    if (hasRemoteParent != null) {
+      spanBuilder.setSameProcessAsParentSpan(BoolValue.of(!hasRemoteParent));
+    }
+
+    SpanId parentSpanId = spanData.getParentSpanId();
+    if (parentSpanId != null && parentSpanId.isValid()) {
+      spanBuilder.setParentSpanId(toByteString(parentSpanId.getBytes()));
+    }
+
+    return spanBuilder.build();
+  }
+
+  @VisibleForTesting
+  static ByteString toByteString(byte[] bytes) {
+    return ByteString.copyFrom(bytes);
+  }
+
+  private static Tracestate toTracestateProto(io.opencensus.trace.Tracestate tracestate) {
+    return Tracestate.newBuilder().addAllEntries(toEntriesProto(tracestate.getEntries())).build();
+  }
+
+  private static List<Entry> toEntriesProto(List<io.opencensus.trace.Tracestate.Entry> entries) {
+    List<Entry> entriesProto = new ArrayList<Entry>();
+    for (io.opencensus.trace.Tracestate.Entry entry : entries) {
+      entriesProto.add(
+          Entry.newBuilder().setKey(entry.getKey()).setValue(entry.getValue()).build());
+    }
+    return entriesProto;
+  }
+
+  private static SpanKind toSpanKindProto(Kind kind) {
+    switch (kind) {
+      case CLIENT:
+        return SpanKind.CLIENT;
+      case SERVER:
+        return SpanKind.SERVER;
+    }
+    return SpanKind.UNRECOGNIZED;
+  }
+
+  private static Span.TimeEvents toTimeEventsProto(
+      TimedEvents<Annotation> annotationTimedEvents,
+      TimedEvents<io.opencensus.trace.MessageEvent> messageEventTimedEvents) {
+    Span.TimeEvents.Builder timeEventsBuilder = Span.TimeEvents.newBuilder();
+    timeEventsBuilder.setDroppedAnnotationsCount(annotationTimedEvents.getDroppedEventsCount());
+    for (TimedEvent<Annotation> annotation : annotationTimedEvents.getEvents()) {
+      timeEventsBuilder.addTimeEvent(toTimeAnnotationProto(annotation));
+    }
+    timeEventsBuilder.setDroppedMessageEventsCount(messageEventTimedEvents.getDroppedEventsCount());
+    for (TimedEvent<io.opencensus.trace.MessageEvent> networkEvent :
+        messageEventTimedEvents.getEvents()) {
+      timeEventsBuilder.addTimeEvent(toTimeMessageEventProto(networkEvent));
+    }
+    return timeEventsBuilder.build();
+  }
+
+  private static TimeEvent toTimeAnnotationProto(TimedEvent<Annotation> timedEvent) {
+    TimeEvent.Builder timeEventBuilder =
+        TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+    Annotation annotation = timedEvent.getEvent();
+    timeEventBuilder.setAnnotation(
+        TimeEvent.Annotation.newBuilder()
+            .setDescription(toTruncatableStringProto(annotation.getDescription()))
+            .setAttributes(toAttributesBuilderProto(annotation.getAttributes(), 0))
+            .build());
+    return timeEventBuilder.build();
+  }
+
+  private static TimeEvent toTimeMessageEventProto(
+      TimedEvent<io.opencensus.trace.MessageEvent> timedEvent) {
+    TimeEvent.Builder timeEventBuilder =
+        TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+    io.opencensus.trace.MessageEvent messageEvent = timedEvent.getEvent();
+    timeEventBuilder.setMessageEvent(
+        TimeEvent.MessageEvent.newBuilder()
+            .setId(messageEvent.getMessageId())
+            .setCompressedSize(messageEvent.getCompressedMessageSize())
+            .setUncompressedSize(messageEvent.getUncompressedMessageSize())
+            .setType(toMessageEventTypeProto(messageEvent))
+            .build());
+    return timeEventBuilder.build();
+  }
+
+  private static TimeEvent.MessageEvent.Type toMessageEventTypeProto(
+      io.opencensus.trace.MessageEvent messageEvent) {
+    if (messageEvent.getType() == Type.RECEIVED) {
+      return MessageEvent.Type.RECEIVED;
+    } else {
+      return MessageEvent.Type.SENT;
+    }
+  }
+
+  private static Attributes toAttributesProto(
+      io.opencensus.trace.export.SpanData.Attributes attributes) {
+    Attributes.Builder attributesBuilder =
+        toAttributesBuilderProto(
+            attributes.getAttributeMap(), attributes.getDroppedAttributesCount());
+    return attributesBuilder.build();
+  }
+
+  private static Attributes.Builder toAttributesBuilderProto(
+      Map<String, io.opencensus.trace.AttributeValue> attributes, int droppedAttributesCount) {
+    Attributes.Builder attributesBuilder =
+        Attributes.newBuilder().setDroppedAttributesCount(droppedAttributesCount);
+    for (Map.Entry<String, io.opencensus.trace.AttributeValue> label : attributes.entrySet()) {
+      AttributeValue value = toAttributeValueProto(label.getValue());
+      if (value != null) {
+        attributesBuilder.putAttributeMap(label.getKey(), value);
+      }
+    }
+    return attributesBuilder;
+  }
+
+  @javax.annotation.Nullable
+  private static AttributeValue toAttributeValueProto(
+      io.opencensus.trace.AttributeValue attributeValue) {
+    return attributeValue.match(
+        stringAttributeValueFunction,
+        booleanAttributeValueFunction,
+        longAttributeValueFunction,
+        doubleAttributeValueFunction,
+        Functions.</*@Nullable*/ AttributeValue>returnNull());
+  }
+
+  private static Status toStatusProto(io.opencensus.trace.Status status) {
+    Status.Builder statusBuilder = Status.newBuilder().setCode(status.getCanonicalCode().value());
+    if (status.getDescription() != null) {
+      statusBuilder.setMessage(status.getDescription());
+    }
+    return statusBuilder.build();
+  }
+
+  @VisibleForTesting
+  static TruncatableString toTruncatableStringProto(String string) {
+    return TruncatableString.newBuilder().setValue(string).setTruncatedByteCount(0).build();
+  }
+
+  static com.google.protobuf.Timestamp toTimestampProto(Timestamp timestamp) {
+    return com.google.protobuf.Timestamp.newBuilder()
+        .setSeconds(timestamp.getSeconds())
+        .setNanos(timestamp.getNanos())
+        .build();
+  }
+
+  private static Link.Type toLinkTypeProto(io.opencensus.trace.Link.Type type) {
+    if (type == io.opencensus.trace.Link.Type.PARENT_LINKED_SPAN) {
+      return Link.Type.PARENT_LINKED_SPAN;
+    } else {
+      return Link.Type.CHILD_LINKED_SPAN;
+    }
+  }
+
+  private static Link toLinkProto(io.opencensus.trace.Link link) {
+    return Link.newBuilder()
+        .setTraceId(toByteString(link.getTraceId().getBytes()))
+        .setSpanId(toByteString(link.getSpanId().getBytes()))
+        .setType(toLinkTypeProto(link.getType()))
+        .setAttributes(toAttributesBuilderProto(link.getAttributes(), 0))
+        .build();
+  }
+
+  private static Links toLinksProto(io.opencensus.trace.export.SpanData.Links links) {
+    final Links.Builder linksBuilder =
+        Links.newBuilder().setDroppedLinksCount(links.getDroppedLinksCount());
+    for (io.opencensus.trace.Link link : links.getLinks()) {
+      linksBuilder.addLink(toLinkProto(link));
+    }
+    return linksBuilder.build();
+  }
+
+  /**
+   * Converts {@link TraceParams} to {@link TraceConfig}.
+   *
+   * @param traceParams the {@code TraceParams}.
+   * @return {@code TraceConfig}.
+   */
+  static TraceConfig toTraceConfigProto(TraceParams traceParams) {
+    TraceConfig.Builder traceConfigProtoBuilder = TraceConfig.newBuilder();
+    Sampler librarySampler = traceParams.getSampler();
+
+    if (Samplers.alwaysSample().equals(librarySampler)) {
+      traceConfigProtoBuilder.setConstantSampler(
+          ConstantSampler.newBuilder().setDecision(true).build());
+    } else if (Samplers.neverSample().equals(librarySampler)) {
+      traceConfigProtoBuilder.setConstantSampler(
+          ConstantSampler.newBuilder().setDecision(false).build());
+    } else {
+      // TODO: consider exposing the sampling probability of ProbabilitySampler.
+      double samplingProbability = parseSamplingProbability(librarySampler);
+      traceConfigProtoBuilder.setProbabilitySampler(
+          ProbabilitySampler.newBuilder().setSamplingProbability(samplingProbability).build());
+    } // TODO: add support for RateLimitingSampler.
+
+    return traceConfigProtoBuilder.build();
+  }
+
+  private static double parseSamplingProbability(Sampler sampler) {
+    String description = sampler.getDescription();
+    // description follows format "ProbabilitySampler{%.6f}", samplingProbability.
+    int leftParenIndex = description.indexOf("{");
+    int rightParenIndex = description.indexOf("}");
+    return Double.parseDouble(description.substring(leftParenIndex + 1, rightParenIndex));
+  }
+
+  /**
+   * Converts {@link TraceConfig} to {@link TraceParams}.
+   *
+   * @param traceConfigProto {@code TraceConfig}.
+   * @param currentTraceParams current {@code TraceParams}.
+   * @return updated {@code TraceParams}.
+   * @since 0.17
+   */
+  static TraceParams fromTraceConfigProto(
+      TraceConfig traceConfigProto, TraceParams currentTraceParams) {
+    TraceParams.Builder builder = currentTraceParams.toBuilder();
+    if (traceConfigProto.hasConstantSampler()) {
+      ConstantSampler constantSampler = traceConfigProto.getConstantSampler();
+      if (Boolean.TRUE.equals(constantSampler.getDecision())) {
+        builder.setSampler(Samplers.alwaysSample());
+      } else {
+        builder.setSampler(Samplers.neverSample());
+      }
+    } else if (traceConfigProto.hasProbabilitySampler()) {
+      builder.setSampler(
+          Samplers.probabilitySampler(
+              traceConfigProto.getProbabilitySampler().getSamplingProbability()));
+    } // TODO: add support for RateLimitingSampler.
+    return builder.build();
+  }
+
+  // Creates a TraceConfig proto message with current TraceParams.
+  static TraceConfig getCurrentTraceConfig(io.opencensus.trace.config.TraceConfig traceConfig) {
+    TraceParams traceParams = traceConfig.getActiveTraceParams();
+    return toTraceConfigProto(traceParams);
+  }
+
+  // Creates an updated TraceParams with the given UpdatedLibraryConfig message and current
+  // TraceParams, then applies the updated TraceParams.
+  static TraceParams getUpdatedTraceParams(
+      UpdatedLibraryConfig config, io.opencensus.trace.config.TraceConfig traceConfig) {
+    TraceParams currentParams = traceConfig.getActiveTraceParams();
+    TraceConfig traceConfigProto = config.getConfig();
+    return fromTraceConfigProto(traceConfigProto, currentParams);
+  }
+
+  private TraceProtoUtils() {}
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java
new file mode 100644
index 0000000..d01dd7e
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/**
+ * This package contains the Java implementation of the OpenCensus Agent (OC-Agent) Trace Exporter.
+ *
+ * <p>WARNING: Currently all the public classes under this package are marked as {@link
+ * io.opencensus.common.ExperimentalApi}. The classes and APIs under {@link
+ * io.opencensus.exporter.trace.ocagent} are likely to get backwards-incompatible updates in the
+ * future. DO NOT USE except for experimental purposes.
+ *
+ * <p>See more details on
+ * https://github.com/census-instrumentation/opencensus-proto/tree/master/src/opencensus/proto/agent.
+ */
+@io.opencensus.common.ExperimentalApi
+package io.opencensus.exporter.trace.ocagent;
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java
new file mode 100644
index 0000000..fbdb35e
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
+import io.grpc.netty.NettyServerBuilder;
+import io.grpc.stub.StreamObserver;
+import io.opencensus.proto.agent.trace.v1.CurrentLibraryConfig;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceRequest;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceResponse;
+import io.opencensus.proto.agent.trace.v1.TraceServiceGrpc;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/** Fake implementation of {@link TraceServiceGrpc}. */
+final class FakeOcAgentTraceServiceGrpcImpl extends TraceServiceGrpc.TraceServiceImplBase {
+
+  private static final Logger logger =
+      Logger.getLogger(FakeOcAgentTraceServiceGrpcImpl.class.getName());
+
+  // Default updatedLibraryConfig uses an always sampler.
+  private UpdatedLibraryConfig updatedLibraryConfig =
+      UpdatedLibraryConfig.newBuilder()
+          .setConfig(
+              TraceConfig.newBuilder()
+                  .setConstantSampler(ConstantSampler.newBuilder().setDecision(true).build())
+                  .build())
+          .build();
+
+  private final List<CurrentLibraryConfig> currentLibraryConfigs = new ArrayList<>();
+  private final List<ExportTraceServiceRequest> exportTraceServiceRequests = new ArrayList<>();
+
+  private final AtomicReference<StreamObserver<UpdatedLibraryConfig>> updatedConfigObserverRef =
+      new AtomicReference<>();
+
+  private final StreamObserver<CurrentLibraryConfig> currentConfigObserver =
+      new StreamObserver<CurrentLibraryConfig>() {
+        @Override
+        public void onNext(CurrentLibraryConfig value) {
+          currentLibraryConfigs.add(value);
+          @Nullable
+          StreamObserver<UpdatedLibraryConfig> updatedConfigObserver =
+              updatedConfigObserverRef.get();
+          if (updatedConfigObserver != null) {
+            updatedConfigObserver.onNext(updatedLibraryConfig);
+          }
+        }
+
+        @Override
+        public void onError(Throwable t) {
+          logger.warning("Exception thrown for config stream: " + t);
+        }
+
+        @Override
+        public void onCompleted() {}
+      };
+
+  private final StreamObserver<ExportTraceServiceRequest> exportRequestObserver =
+      new StreamObserver<ExportTraceServiceRequest>() {
+        @Override
+        public void onNext(ExportTraceServiceRequest value) {
+          exportTraceServiceRequests.add(value);
+        }
+
+        @Override
+        public void onError(Throwable t) {
+          logger.warning("Exception thrown for export stream: " + t);
+        }
+
+        @Override
+        public void onCompleted() {}
+      };
+
+  @Override
+  public StreamObserver<CurrentLibraryConfig> config(
+      StreamObserver<UpdatedLibraryConfig> updatedLibraryConfigStreamObserver) {
+    updatedConfigObserverRef.set(updatedLibraryConfigStreamObserver);
+    return currentConfigObserver;
+  }
+
+  @Override
+  public StreamObserver<ExportTraceServiceRequest> export(
+      StreamObserver<ExportTraceServiceResponse> exportTraceServiceResponseStreamObserver) {
+    return exportRequestObserver;
+  }
+
+  // Returns the stored CurrentLibraryConfigs.
+  List<CurrentLibraryConfig> getCurrentLibraryConfigs() {
+    return Collections.unmodifiableList(currentLibraryConfigs);
+  }
+
+  // Returns the stored ExportTraceServiceRequests.
+  List<ExportTraceServiceRequest> getExportTraceServiceRequests() {
+    return Collections.unmodifiableList(exportTraceServiceRequests);
+  }
+
+  // Sets the UpdatedLibraryConfig that will be passed to client.
+  void setUpdatedLibraryConfig(UpdatedLibraryConfig updatedLibraryConfig) {
+    this.updatedLibraryConfig = updatedLibraryConfig;
+  }
+
+  // Gets the UpdatedLibraryConfig that will be passed to client.
+  UpdatedLibraryConfig getUpdatedLibraryConfig() {
+    return updatedLibraryConfig;
+  }
+
+  static void startServer(String endPoint) throws IOException {
+    ServerBuilder<?> builder = NettyServerBuilder.forAddress(parseEndpoint(endPoint));
+    Executor executor = MoreExecutors.directExecutor();
+    builder.executor(executor);
+    final Server server = builder.addService(new FakeOcAgentTraceServiceGrpcImpl()).build();
+    server.start();
+    logger.info("Server started at " + endPoint);
+
+    Runtime.getRuntime()
+        .addShutdownHook(
+            new Thread() {
+              @Override
+              public void run() {
+                server.shutdown();
+              }
+            });
+
+    try {
+      server.awaitTermination();
+    } catch (InterruptedException e) {
+      logger.warning("Thread interrupted: " + e.getMessage());
+      Thread.currentThread().interrupt();
+    }
+  }
+
+  private static InetSocketAddress parseEndpoint(String endPoint) {
+    try {
+      int colonIndex = endPoint.indexOf(":");
+      String host = endPoint.substring(0, colonIndex);
+      int port = Integer.parseInt(endPoint.substring(colonIndex + 1));
+      return new InetSocketAddress(host, port);
+    } catch (RuntimeException e) {
+      logger.warning("Unexpected format of end point: " + endPoint + ", use default end point.");
+      return new InetSocketAddress("localhost", 55678);
+    }
+  }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java
new file mode 100644
index 0000000..f619021
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.grpc.stub.StreamObserver;
+import io.opencensus.proto.agent.trace.v1.CurrentLibraryConfig;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceRequest;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceResponse;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link FakeOcAgentTraceServiceGrpcImpl}. */
+@RunWith(JUnit4.class)
+public class FakeOcAgentTraceServiceGrpcImplTest {
+
+  private final List<UpdatedLibraryConfig> updatedLibraryConfigs = new ArrayList<>();
+
+  private final StreamObserver<UpdatedLibraryConfig> updatedConfigObserver =
+      new StreamObserver<UpdatedLibraryConfig>() {
+
+        @Override
+        public void onNext(UpdatedLibraryConfig value) {
+          updatedLibraryConfigs.add(value);
+        }
+
+        @Override
+        public void onError(Throwable t) {}
+
+        @Override
+        public void onCompleted() {}
+      };
+
+  private final StreamObserver<ExportTraceServiceResponse> exportResponseObserver =
+      new StreamObserver<ExportTraceServiceResponse>() {
+        @Override
+        public void onNext(ExportTraceServiceResponse value) {}
+
+        @Override
+        public void onError(Throwable t) {}
+
+        @Override
+        public void onCompleted() {}
+      };
+
+  private static final UpdatedLibraryConfig neverSampledLibraryConfig =
+      UpdatedLibraryConfig.newBuilder()
+          .setConfig(
+              TraceConfig.newBuilder()
+                  .setConstantSampler(ConstantSampler.newBuilder().setDecision(false).build())
+                  .build())
+          .build();
+
+  @Test
+  public void export() {
+    FakeOcAgentTraceServiceGrpcImpl traceServiceGrpc = new FakeOcAgentTraceServiceGrpcImpl();
+    StreamObserver<ExportTraceServiceRequest> exportRequestObserver =
+        traceServiceGrpc.export(exportResponseObserver);
+    ExportTraceServiceRequest request = ExportTraceServiceRequest.getDefaultInstance();
+    exportRequestObserver.onNext(request);
+    assertThat(traceServiceGrpc.getExportTraceServiceRequests()).containsExactly(request);
+  }
+
+  @Test
+  public void config() {
+    FakeOcAgentTraceServiceGrpcImpl traceServiceGrpc = new FakeOcAgentTraceServiceGrpcImpl();
+    StreamObserver<CurrentLibraryConfig> currentConfigObsever =
+        traceServiceGrpc.config(updatedConfigObserver);
+    CurrentLibraryConfig currentLibraryConfig = CurrentLibraryConfig.getDefaultInstance();
+    currentConfigObsever.onNext(currentLibraryConfig);
+    assertThat(traceServiceGrpc.getCurrentLibraryConfigs()).containsExactly(currentLibraryConfig);
+    assertThat(updatedLibraryConfigs).containsExactly(traceServiceGrpc.getUpdatedLibraryConfig());
+    updatedLibraryConfigs.clear();
+  }
+
+  @Test
+  public void config_WithNeverSampler() {
+    FakeOcAgentTraceServiceGrpcImpl traceServiceGrpc = new FakeOcAgentTraceServiceGrpcImpl();
+    traceServiceGrpc.setUpdatedLibraryConfig(neverSampledLibraryConfig);
+    StreamObserver<CurrentLibraryConfig> currentConfigObsever =
+        traceServiceGrpc.config(updatedConfigObserver);
+    CurrentLibraryConfig currentLibraryConfig = CurrentLibraryConfig.getDefaultInstance();
+    currentConfigObsever.onNext(currentLibraryConfig);
+    assertThat(traceServiceGrpc.getCurrentLibraryConfigs()).containsExactly(currentLibraryConfig);
+    assertThat(updatedLibraryConfigs).containsExactly(neverSampledLibraryConfig);
+    updatedLibraryConfigs.clear();
+  }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java
new file mode 100644
index 0000000..813066b
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.trace.ocagent.OcAgentNodeUtils.OC_AGENT_EXPORTER_VERSION;
+import static io.opencensus.exporter.trace.ocagent.OcAgentNodeUtils.RESOURCE_LABEL_ATTRIBUTE_KEY;
+import static io.opencensus.exporter.trace.ocagent.OcAgentNodeUtils.RESOURCE_TYPE_ATTRIBUTE_KEY;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.proto.agent.common.v1.LibraryInfo;
+import io.opencensus.proto.agent.common.v1.LibraryInfo.Language;
+import io.opencensus.proto.agent.common.v1.ProcessIdentifier;
+import io.opencensus.proto.agent.common.v1.ServiceInfo;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link OcAgentNodeUtils}. */
+@RunWith(JUnit4.class)
+public class OcAgentNodeUtilsTest {
+
+  private static final AwsEc2InstanceMonitoredResource AWS_RESOURCE =
+      AwsEc2InstanceMonitoredResource.create("account1", "instance1", "us-east-2");
+  private static final GcpGceInstanceMonitoredResource GCE_RESOURCE =
+      GcpGceInstanceMonitoredResource.create("account2", "instance2", "us-west2");
+  private static final GcpGkeContainerMonitoredResource GKE_RESOURCE =
+      GcpGkeContainerMonitoredResource.create(
+          "account3", "cluster", "container", "", "instance3", "", "us-west4");
+
+  @Test
+  public void testConstants() {
+    assertThat(OC_AGENT_EXPORTER_VERSION).isEqualTo("0.17.0-SNAPSHOT");
+    assertThat(RESOURCE_TYPE_ATTRIBUTE_KEY).isEqualTo("OPENCENSUS_SOURCE_TYPE");
+    assertThat(RESOURCE_LABEL_ATTRIBUTE_KEY).isEqualTo("OPENCENSUS_SOURCE_LABELS");
+  }
+
+  @Test
+  public void getProcessIdentifier() {
+    String jvmName = "54321@my.org";
+    Timestamp timestamp = Timestamp.create(10, 20);
+    ProcessIdentifier processIdentifier = OcAgentNodeUtils.getProcessIdentifier(jvmName, timestamp);
+    assertThat(processIdentifier.getHostName()).isEqualTo("my.org");
+    assertThat(processIdentifier.getPid()).isEqualTo(54321);
+    assertThat(processIdentifier.getStartTimestamp())
+        .isEqualTo(com.google.protobuf.Timestamp.newBuilder().setSeconds(10).setNanos(20).build());
+  }
+
+  @Test
+  public void getLibraryInfo() {
+    String currentOcJavaVersion = "0.16.0";
+    LibraryInfo libraryInfo = OcAgentNodeUtils.getLibraryInfo(currentOcJavaVersion);
+    assertThat(libraryInfo.getLanguage()).isEqualTo(Language.JAVA);
+    assertThat(libraryInfo.getCoreLibraryVersion()).isEqualTo(currentOcJavaVersion);
+    assertThat(libraryInfo.getExporterVersion()).isEqualTo(OC_AGENT_EXPORTER_VERSION);
+  }
+
+  @Test
+  public void getServiceInfo() {
+    String serviceName = "my-service";
+    ServiceInfo serviceInfo = OcAgentNodeUtils.getServiceInfo(serviceName);
+    assertThat(serviceInfo.getName()).isEqualTo(serviceName);
+  }
+
+  @Test
+  public void getAttributeMap_Null() {
+    Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(null);
+    assertThat(attributeMap).isEmpty();
+  }
+
+  @Test
+  public void getAttributeMap_AwsEc2Resource() {
+    Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(AWS_RESOURCE);
+    assertThat(attributeMap)
+        .containsExactly(
+            RESOURCE_TYPE_ATTRIBUTE_KEY,
+            "AWS_EC2_INSTANCE",
+            RESOURCE_LABEL_ATTRIBUTE_KEY,
+            "aws_account=account1,instance_id=instance1,region=us-east-2");
+  }
+
+  @Test
+  public void getAttributeMap_GceResource() {
+    Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(GCE_RESOURCE);
+    assertThat(attributeMap)
+        .containsExactly(
+            RESOURCE_TYPE_ATTRIBUTE_KEY,
+            "GCP_GCE_INSTANCE",
+            RESOURCE_LABEL_ATTRIBUTE_KEY,
+            "gcp_account=account2,instance_id=instance2,zone=us-west2");
+  }
+
+  @Test
+  public void getAttributeMap_GkeResource() {
+    Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(GKE_RESOURCE);
+    assertThat(attributeMap)
+        .containsExactly(
+            RESOURCE_TYPE_ATTRIBUTE_KEY,
+            "GCP_GKE_CONTAINER",
+            RESOURCE_LABEL_ATTRIBUTE_KEY,
+            "gcp_account=account3,instance_id=instance3,location=us-west4,"
+                + "cluster_name=cluster,container_name=container");
+  }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java
new file mode 100644
index 0000000..81bc5c6
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link OcAgentTraceExporterConfiguration}. */
+@RunWith(JUnit4.class)
+public class OcAgentTraceExporterConfigurationTest {
+
+  @Test
+  public void defaultConfiguration() {
+    OcAgentTraceExporterConfiguration configuration =
+        OcAgentTraceExporterConfiguration.builder().build();
+    assertThat(configuration.getEndPoint()).isNull();
+    assertThat(configuration.getServiceName()).isNull();
+    assertThat(configuration.getUseInsecure()).isNull();
+    assertThat(configuration.getRetryInterval()).isNull();
+    assertThat(configuration.getEnableConfig()).isTrue();
+  }
+
+  @Test
+  public void setAndGet() {
+    Duration oneMinute = Duration.create(60, 0);
+    OcAgentTraceExporterConfiguration configuration =
+        OcAgentTraceExporterConfiguration.builder()
+            .setEndPoint("192.168.0.1:50051")
+            .setServiceName("service")
+            .setUseInsecure(true)
+            .setRetryInterval(oneMinute)
+            .setEnableConfig(false)
+            .build();
+    assertThat(configuration.getEndPoint()).isEqualTo("192.168.0.1:50051");
+    assertThat(configuration.getServiceName()).isEqualTo("service");
+    assertThat(configuration.getUseInsecure()).isTrue();
+    assertThat(configuration.getRetryInterval()).isEqualTo(oneMinute);
+    assertThat(configuration.getEnableConfig()).isFalse();
+  }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java
new file mode 100644
index 0000000..c58acdb
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link OcAgentTraceExporter}. */
+@RunWith(JUnit4.class)
+public class OcAgentTraceExporterTest {
+  @Mock private SpanExporter spanExporter;
+  @Mock private Handler handler;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void registerUnregisterOcAgentTraceExporter() {
+    OcAgentTraceExporter.register(spanExporter, handler);
+    verify(spanExporter)
+        .registerHandler(
+            eq("io.opencensus.exporter.trace.ocagent.OcAgentTraceExporter"),
+            any(OcAgentTraceExporterHandler.class));
+    OcAgentTraceExporter.unregister(spanExporter);
+    verify(spanExporter)
+        .unregisterHandler(eq("io.opencensus.exporter.trace.ocagent.OcAgentTraceExporter"));
+  }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java
new file mode 100644
index 0000000..74c7c29
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.trace.ocagent.TraceProtoUtils.toByteString;
+import static io.opencensus.exporter.trace.ocagent.TraceProtoUtils.toTruncatableStringProto;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.UInt32Value;
+import io.opencensus.common.Timestamp;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.AttributeValue;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.ProbabilitySampler;
+import io.opencensus.proto.trace.v1.Span;
+import io.opencensus.proto.trace.v1.Span.SpanKind;
+import io.opencensus.proto.trace.v1.Span.TimeEvent;
+import io.opencensus.proto.trace.v1.Span.TimeEvent.MessageEvent;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link TraceProtoUtils}. */
+@RunWith(JUnit4.class)
+public class TraceProtoUtilsTest {
+
+  @Mock private io.opencensus.trace.config.TraceConfig mockTraceConfig;
+
+  private static final TraceParams DEFAULT_PARAMS = TraceParams.DEFAULT;
+
+  private static final Timestamp startTimestamp = Timestamp.create(123, 456);
+  private static final Timestamp eventTimestamp1 = Timestamp.create(123, 457);
+  private static final Timestamp eventTimestamp2 = Timestamp.create(123, 458);
+  private static final Timestamp eventTimestamp3 = Timestamp.create(123, 459);
+  private static final Timestamp endTimestamp = Timestamp.create(123, 460);
+
+  private static final String TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736";
+  private static final String SPAN_ID = "24aa0b2d371f48c9";
+  private static final String PARENT_SPAN_ID = "71da8d631536f5f1";
+  private static final String SPAN_NAME = "MySpanName";
+  private static final String ANNOTATION_TEXT = "MyAnnotationText";
+  private static final String ATTRIBUTE_KEY_1 = "MyAttributeKey1";
+  private static final String ATTRIBUTE_KEY_2 = "MyAttributeKey2";
+
+  private static final String FIRST_KEY = "key_1";
+  private static final String SECOND_KEY = "key_2";
+  private static final String FIRST_VALUE = "value.1";
+  private static final String SECOND_VALUE = "value.2";
+  private static final Tracestate multiValueTracestate =
+      Tracestate.builder().set(FIRST_KEY, FIRST_VALUE).set(SECOND_KEY, SECOND_VALUE).build();
+
+  private static final int DROPPED_ATTRIBUTES_COUNT = 1;
+  private static final int DROPPED_ANNOTATIONS_COUNT = 2;
+  private static final int DROPPED_NETWORKEVENTS_COUNT = 3;
+  private static final int DROPPED_LINKS_COUNT = 4;
+  private static final int CHILD_SPAN_COUNT = 13;
+
+  private static final Annotation annotation = Annotation.fromDescription(ANNOTATION_TEXT);
+  private static final io.opencensus.trace.MessageEvent recvMessageEvent =
+      io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.RECEIVED, 1)
+          .build();
+  private static final io.opencensus.trace.MessageEvent sentMessageEvent =
+      io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.SENT, 1)
+          .build();
+  private static final Status status = Status.DEADLINE_EXCEEDED.withDescription("TooSlow");
+  private static final SpanId parentSpanId = SpanId.fromLowerBase16(PARENT_SPAN_ID);
+  private static final SpanId spanId = SpanId.fromLowerBase16(SPAN_ID);
+  private static final TraceId traceId = TraceId.fromLowerBase16(TRACE_ID);
+  private static final TraceOptions traceOptions = TraceOptions.DEFAULT;
+  private static final SpanContext spanContext =
+      SpanContext.create(traceId, spanId, traceOptions, multiValueTracestate);
+
+  private static final List<TimedEvent<Annotation>> annotationsList =
+      ImmutableList.of(
+          SpanData.TimedEvent.create(eventTimestamp1, annotation),
+          SpanData.TimedEvent.create(eventTimestamp3, annotation));
+  private static final List<TimedEvent<io.opencensus.trace.MessageEvent>> networkEventsList =
+      ImmutableList.of(
+          SpanData.TimedEvent.create(eventTimestamp1, recvMessageEvent),
+          SpanData.TimedEvent.create(eventTimestamp2, sentMessageEvent));
+  private static final List<Link> linksList =
+      ImmutableList.of(Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN));
+
+  private static final SpanData.Attributes attributes =
+      SpanData.Attributes.create(
+          ImmutableMap.of(
+              ATTRIBUTE_KEY_1,
+              io.opencensus.trace.AttributeValue.longAttributeValue(10L),
+              ATTRIBUTE_KEY_2,
+              io.opencensus.trace.AttributeValue.booleanAttributeValue(true)),
+          DROPPED_ATTRIBUTES_COUNT);
+  private static final TimedEvents<Annotation> annotations =
+      TimedEvents.create(annotationsList, DROPPED_ANNOTATIONS_COUNT);
+  private static final TimedEvents<io.opencensus.trace.MessageEvent> messageEvents =
+      TimedEvents.create(networkEventsList, DROPPED_NETWORKEVENTS_COUNT);
+  private static final SpanData.Links links = SpanData.Links.create(linksList, DROPPED_LINKS_COUNT);
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    Mockito.when(mockTraceConfig.getActiveTraceParams()).thenReturn(DEFAULT_PARAMS);
+    Mockito.doNothing()
+        .when(mockTraceConfig)
+        .updateActiveTraceParams(Mockito.any(TraceParams.class));
+  }
+
+  @Test
+  public void toSpanProto() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            /* hasRemoteParent= */ false,
+            SPAN_NAME,
+            Kind.CLIENT,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    TimeEvent annotationTimeEvent1 =
+        TimeEvent.newBuilder()
+            .setAnnotation(
+                TimeEvent.Annotation.newBuilder()
+                    .setDescription(toTruncatableStringProto(ANNOTATION_TEXT))
+                    .setAttributes(Span.Attributes.newBuilder().build())
+                    .build())
+            .setTime(
+                com.google.protobuf.Timestamp.newBuilder()
+                    .setSeconds(eventTimestamp1.getSeconds())
+                    .setNanos(eventTimestamp1.getNanos())
+                    .build())
+            .build();
+    TimeEvent annotationTimeEvent2 =
+        TimeEvent.newBuilder()
+            .setAnnotation(
+                TimeEvent.Annotation.newBuilder()
+                    .setDescription(toTruncatableStringProto(ANNOTATION_TEXT))
+                    .setAttributes(Span.Attributes.newBuilder().build())
+                    .build())
+            .setTime(
+                com.google.protobuf.Timestamp.newBuilder()
+                    .setSeconds(eventTimestamp3.getSeconds())
+                    .setNanos(eventTimestamp3.getNanos())
+                    .build())
+            .build();
+
+    TimeEvent sentTimeEvent =
+        TimeEvent.newBuilder()
+            .setMessageEvent(
+                TimeEvent.MessageEvent.newBuilder()
+                    .setType(MessageEvent.Type.SENT)
+                    .setId(sentMessageEvent.getMessageId()))
+            .setTime(
+                com.google.protobuf.Timestamp.newBuilder()
+                    .setSeconds(eventTimestamp2.getSeconds())
+                    .setNanos(eventTimestamp2.getNanos())
+                    .build())
+            .build();
+    TimeEvent recvTimeEvent =
+        TimeEvent.newBuilder()
+            .setMessageEvent(
+                TimeEvent.MessageEvent.newBuilder()
+                    .setType(MessageEvent.Type.RECEIVED)
+                    .setId(recvMessageEvent.getMessageId()))
+            .setTime(
+                com.google.protobuf.Timestamp.newBuilder()
+                    .setSeconds(eventTimestamp1.getSeconds())
+                    .setNanos(eventTimestamp1.getNanos())
+                    .build())
+            .build();
+
+    Span.Links spanLinks =
+        Span.Links.newBuilder()
+            .setDroppedLinksCount(DROPPED_LINKS_COUNT)
+            .addLink(
+                Span.Link.newBuilder()
+                    .setType(Span.Link.Type.CHILD_LINKED_SPAN)
+                    .setTraceId(toByteString(traceId.getBytes()))
+                    .setSpanId(toByteString(spanId.getBytes()))
+                    .setAttributes(Span.Attributes.newBuilder().build())
+                    .build())
+            .build();
+
+    io.opencensus.proto.trace.v1.Status spanStatus =
+        io.opencensus.proto.trace.v1.Status.newBuilder()
+            .setCode(com.google.rpc.Code.DEADLINE_EXCEEDED.getNumber())
+            .setMessage("TooSlow")
+            .build();
+
+    com.google.protobuf.Timestamp startTime =
+        com.google.protobuf.Timestamp.newBuilder()
+            .setSeconds(startTimestamp.getSeconds())
+            .setNanos(startTimestamp.getNanos())
+            .build();
+    com.google.protobuf.Timestamp endTime =
+        com.google.protobuf.Timestamp.newBuilder()
+            .setSeconds(endTimestamp.getSeconds())
+            .setNanos(endTimestamp.getNanos())
+            .build();
+
+    Span span = TraceProtoUtils.toSpanProto(spanData);
+    assertThat(span.getName()).isEqualTo(toTruncatableStringProto(SPAN_NAME));
+    assertThat(span.getTraceId()).isEqualTo(toByteString(traceId.getBytes()));
+    assertThat(span.getSpanId()).isEqualTo(toByteString(spanId.getBytes()));
+    assertThat(span.getParentSpanId()).isEqualTo(toByteString(parentSpanId.getBytes()));
+    assertThat(span.getStartTime()).isEqualTo(startTime);
+    assertThat(span.getEndTime()).isEqualTo(endTime);
+    assertThat(span.getKind()).isEqualTo(SpanKind.CLIENT);
+    assertThat(span.getAttributes().getDroppedAttributesCount())
+        .isEqualTo(DROPPED_ATTRIBUTES_COUNT);
+    // The generated attributes map contains more values (e.g. agent). We only test what we added.
+    assertThat(span.getAttributes().getAttributeMapMap())
+        .containsEntry(ATTRIBUTE_KEY_1, AttributeValue.newBuilder().setIntValue(10L).build());
+    assertThat(span.getAttributes().getAttributeMapMap())
+        .containsEntry(ATTRIBUTE_KEY_2, AttributeValue.newBuilder().setBoolValue(true).build());
+    assertThat(span.getTimeEvents().getDroppedMessageEventsCount())
+        .isEqualTo(DROPPED_NETWORKEVENTS_COUNT);
+    assertThat(span.getTimeEvents().getDroppedAnnotationsCount())
+        .isEqualTo(DROPPED_ANNOTATIONS_COUNT);
+    assertThat(span.getTimeEvents().getTimeEventList())
+        .containsAllOf(annotationTimeEvent1, annotationTimeEvent2, sentTimeEvent, recvTimeEvent);
+    assertThat(span.getLinks()).isEqualTo(spanLinks);
+    assertThat(span.getStatus()).isEqualTo(spanStatus);
+    assertThat(span.getSameProcessAsParentSpan()).isEqualTo(BoolValue.of(true));
+    assertThat(span.getChildSpanCount())
+        .isEqualTo(UInt32Value.newBuilder().setValue(CHILD_SPAN_COUNT).build());
+  }
+
+  @Test
+  public void toTraceConfigProto_AlwaysSampler() {
+    assertThat(TraceProtoUtils.toTraceConfigProto(getTraceParams(Samplers.alwaysSample())))
+        .isEqualTo(
+            TraceConfig.newBuilder()
+                .setConstantSampler(ConstantSampler.newBuilder().setDecision(true).build())
+                .build());
+  }
+
+  @Test
+  public void toTraceConfigProto_NeverSampler() {
+    assertThat(TraceProtoUtils.toTraceConfigProto(getTraceParams(Samplers.neverSample())))
+        .isEqualTo(
+            TraceConfig.newBuilder()
+                .setConstantSampler(ConstantSampler.newBuilder().setDecision(false).build())
+                .build());
+  }
+
+  @Test
+  public void toTraceConfigProto_ProbabilitySampler() {
+    assertThat(TraceProtoUtils.toTraceConfigProto(getTraceParams(Samplers.probabilitySampler(0.5))))
+        .isEqualTo(
+            TraceConfig.newBuilder()
+                .setProbabilitySampler(
+                    ProbabilitySampler.newBuilder().setSamplingProbability(0.5).build())
+                .build());
+  }
+
+  @Test
+  public void fromTraceConfigProto_AlwaysSampler() {
+    TraceConfig traceConfig =
+        TraceConfig.newBuilder()
+            .setConstantSampler(ConstantSampler.newBuilder().setDecision(true).build())
+            .build();
+    assertThat(TraceProtoUtils.fromTraceConfigProto(traceConfig, DEFAULT_PARAMS).getSampler())
+        .isEqualTo(Samplers.alwaysSample());
+  }
+
+  @Test
+  public void fromTraceConfigProto_NeverSampler() {
+    TraceConfig traceConfig =
+        TraceConfig.newBuilder()
+            .setConstantSampler(ConstantSampler.newBuilder().setDecision(false).build())
+            .build();
+    assertThat(TraceProtoUtils.fromTraceConfigProto(traceConfig, DEFAULT_PARAMS).getSampler())
+        .isEqualTo(Samplers.neverSample());
+  }
+
+  @Test
+  public void fromTraceConfigProto_ProbabilitySampler() {
+    TraceConfig traceConfig =
+        TraceConfig.newBuilder()
+            .setProbabilitySampler(
+                ProbabilitySampler.newBuilder().setSamplingProbability(0.01).build())
+            .build();
+    assertThat(TraceProtoUtils.fromTraceConfigProto(traceConfig, DEFAULT_PARAMS).getSampler())
+        .isEqualTo(Samplers.probabilitySampler(0.01));
+  }
+
+  @Test
+  public void getCurrentTraceConfig() {
+    TraceConfig configProto = TraceProtoUtils.toTraceConfigProto(DEFAULT_PARAMS);
+    assertThat(TraceProtoUtils.getCurrentTraceConfig(mockTraceConfig)).isEqualTo(configProto);
+    Mockito.verify(mockTraceConfig, Mockito.times(1)).getActiveTraceParams();
+  }
+
+  @Test
+  public void applyUpdatedConfig() {
+    TraceConfig configProto =
+        TraceConfig.newBuilder()
+            .setProbabilitySampler(
+                ProbabilitySampler.newBuilder().setSamplingProbability(0.01).build())
+            .build();
+    UpdatedLibraryConfig updatedLibraryConfig =
+        UpdatedLibraryConfig.newBuilder().setConfig(configProto).build();
+    TraceParams traceParams =
+        TraceProtoUtils.getUpdatedTraceParams(updatedLibraryConfig, mockTraceConfig);
+    TraceParams expectedParams =
+        DEFAULT_PARAMS.toBuilder().setSampler(Samplers.probabilitySampler(0.01)).build();
+    Mockito.verify(mockTraceConfig, Mockito.times(1)).getActiveTraceParams();
+    assertThat(traceParams).isEqualTo(expectedParams);
+  }
+
+  private static TraceParams getTraceParams(Sampler sampler) {
+    return DEFAULT_PARAMS.toBuilder().setSampler(sampler).build();
+  }
+}
diff --git a/exporters/trace/stackdriver/README.md b/exporters/trace/stackdriver/README.md
new file mode 100644
index 0000000..9186a47
--- /dev/null
+++ b/exporters/trace/stackdriver/README.md
@@ -0,0 +1,127 @@
+# OpenCensus Stackdriver Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Stackdriver Trace Exporter* is a trace exporter that exports data to 
+Stackdriver Trace. [Stackdriver Trace][stackdriver-trace] is a distributed 
+tracing system that collects latency data from your applications and displays it in the Google 
+Cloud Platform Console. You can track how requests propagate through your application and receive
+detailed near real-time performance insights.
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you must have an application that you'd like to trace. The app can be on 
+Google Cloud Platform, on-premise, or another cloud platform.
+
+In order to be able to push your traces to [Stackdriver Trace][stackdriver-trace], you must:
+
+1. [Create a Cloud project](https://support.google.com/cloud/answer/6251787?hl=en).
+2. [Enable billing](https://support.google.com/cloud/answer/6288653#new-billing).
+3. [Enable the Stackdriver Trace API](https://console.cloud.google.com/apis/api/cloudtrace.googleapis.com/overview).
+
+These steps enable the API but don't require that your app is hosted on Google Cloud Platform.
+
+### Hello "Stackdriver Trace"
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-exporter-trace-stackdriver</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-stackdriver:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This uses the default configuration for authentication and project ID.
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) throws Exception {
+    StackdriverTraceExporter.createAndRegister(
+        StackdriverTraceConfiguration.builder().build());
+    // ...
+  }
+}
+```
+
+#### Authentication
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the authentication see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#authentication).
+
+If you prefer to manually set the credentials use:
+```
+StackdriverTraceExporter.createAndRegisterWithCredentialsAndProjectId(
+    new GoogleCredentials(new AccessToken(accessToken, expirationTime)),
+    "MyStackdriverProjectId");
+```
+
+#### Specifying a Project ID
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the project ID see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id).
+
+If you prefer to manually set the project ID use:
+```
+StackdriverTraceExporter.createAndRegisterWithProjectId("MyStackdriverProjectId");
+```
+
+#### Enable Stackdriver Trace API access scope on Google Cloud Platform
+If your Stackdriver Trace Exporter is running on Kubernetes Engine or Compute Engine,
+you might need additional setup to explicitly enable the ```trace.append``` Stackdriver 
+Trace API access scope. To do that, please follow the instructions for 
+[GKE](https://cloud.google.com/trace/docs/setup/java#kubernetes_engine) or 
+[GCE](https://cloud.google.com/trace/docs/setup/java#compute_engine).
+
+#### Java Versions
+
+Java 7 or above is required for using this exporter.
+
+## FAQ
+### Why do I not see some trace events in Stackdriver?
+In all the versions before '0.9.1' the Stackdriver Trace exporter was implemented using the [v1 
+API][stackdriver-v1-api-url] which is not fully compatible with the OpenCensus data model. Trace 
+events like Annotations and NetworkEvents will be dropped.
+
+### Why do I get a "StatusRuntimeException: NOT_FOUND: Requested entity was not found"?
+One of the possible reasons is you are using a project id with bad format for the exporter.
+Please double check the project id associated with the Stackdriver Trace exporter first. 
+Stackdriver Trace backend will not do any sanitization or trimming on the incoming project id.
+Project id with leading or trailing spaces will be treated as a separate non-existing project
+(e.g "project-id" vs "project-id "), and will cause a NOT_FOUND exception.
+
+[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-exporter-trace-stackdriver/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-stackdriver
+[stackdriver-trace]: https://cloud.google.com/trace/
+[stackdriver-v1-api-url]: https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools.cloudtrace.v1#google.devtools.cloudtrace.v1.TraceSpan
diff --git a/exporters/trace/stackdriver/build.gradle b/exporters/trace/stackdriver/build.gradle
new file mode 100644
index 0000000..83dc970
--- /dev/null
+++ b/exporters/trace/stackdriver/build.gradle
@@ -0,0 +1,31 @@
+description = 'OpenCensus Trace Stackdriver Exporter'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.7
+    it.targetCompatibility = 1.7
+}
+
+dependencies {
+    compileOnly libraries.auto_value
+
+    compile project(':opencensus-api'),
+            project(':opencensus-contrib-monitored-resource-util'),
+            libraries.google_auth,
+            libraries.guava
+
+    compile (libraries.google_cloud_trace) {
+        // Prefer library version.
+        exclude group: 'com.google.guava', module: 'guava'
+
+        // Prefer library version.
+        exclude group: 'com.google.code.findbugs', module: 'jsr305'
+
+        // We will always be more up to date.
+        exclude group: 'io.opencensus', module: 'opencensus-api'
+    }
+
+    testCompile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java
new file mode 100644
index 0000000..8797cc7
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java
@@ -0,0 +1,148 @@
+/*
+ * 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.exporter.trace.stackdriver;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.io.IOException;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Stackdriver Trace.
+ *
+ * <p>Example of usage on Google Cloud VMs:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ *   StackdriverExporter.createAndRegisterWithProjectId("MyStackdriverProjectId");
+ *   ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @deprecated Deprecated due to inconsistent naming. Use {@link StackdriverTraceExporter}.
+ * @since 0.6
+ */
+@Deprecated
+public final class StackdriverExporter {
+
+  /**
+   * Creates and registers the Stackdriver Trace exporter to the OpenCensus library for an explicit
+   * project ID and using explicit credentials. Only one Stackdriver exporter can be registered at
+   * any point.
+   *
+   * @param credentials a credentials used to authenticate API calls.
+   * @param projectId the cloud project id.
+   * @throws IllegalStateException if a Stackdriver exporter is already registered.
+   * @since 0.6
+   */
+  public static void createAndRegisterWithCredentialsAndProjectId(
+      Credentials credentials, String projectId) throws IOException {
+    StackdriverTraceExporter.createAndRegister(
+        StackdriverTraceConfiguration.builder()
+            .setCredentials(credentials)
+            .setProjectId(projectId)
+            .build());
+  }
+
+  /**
+   * Creates and registers the Stackdriver Trace exporter to the OpenCensus library for an explicit
+   * project ID. Only one Stackdriver exporter can be registered at any point.
+   *
+   * <p>This uses the default application credentials see {@link
+   * GoogleCredentials#getApplicationDefault}.
+   *
+   * <p>This is equivalent with:
+   *
+   * <pre>{@code
+   * StackdriverExporter.createAndRegisterWithCredentialsAndProjectId(
+   *     GoogleCredentials.getApplicationDefault(), projectId);
+   * }</pre>
+   *
+   * @param projectId the cloud project id.
+   * @throws IllegalStateException if a Stackdriver exporter is already registered.
+   * @since 0.6
+   */
+  public static void createAndRegisterWithProjectId(String projectId) throws IOException {
+    StackdriverTraceExporter.createAndRegister(
+        StackdriverTraceConfiguration.builder()
+            .setCredentials(GoogleCredentials.getApplicationDefault())
+            .setProjectId(projectId)
+            .build());
+  }
+
+  /**
+   * Creates and registers the Stackdriver Trace exporter to the OpenCensus library. Only one
+   * Stackdriver exporter can be registered at any point.
+   *
+   * <p>This uses the default application credentials see {@link
+   * GoogleCredentials#getApplicationDefault}.
+   *
+   * <p>This uses the default project ID configured see {@link ServiceOptions#getDefaultProjectId}.
+   *
+   * <p>This is equivalent with:
+   *
+   * <pre>{@code
+   * StackdriverExporter.createAndRegisterWithProjectId(ServiceOptions.getDefaultProjectId());
+   * }</pre>
+   *
+   * @throws IllegalStateException if a Stackdriver exporter is already registered.
+   * @since 0.6
+   */
+  public static void createAndRegister() throws IOException {
+    StackdriverTraceExporter.createAndRegister(
+        StackdriverTraceConfiguration.builder()
+            .setCredentials(GoogleCredentials.getApplicationDefault())
+            .setProjectId(ServiceOptions.getDefaultProjectId())
+            .build());
+  }
+
+  /**
+   * Registers the {@code StackdriverExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+   */
+  @VisibleForTesting
+  static void register(SpanExporter spanExporter, Handler handler) {
+    StackdriverTraceExporter.register(spanExporter, handler);
+  }
+
+  /**
+   * Unregisters the Stackdriver Trace exporter from the OpenCensus library.
+   *
+   * @throws IllegalStateException if a Stackdriver exporter is not registered.
+   * @since 0.6
+   */
+  public static void unregister() {
+    StackdriverTraceExporter.unregister();
+  }
+
+  /**
+   * Unregisters the {@code StackdriverExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+   *     unregistered.
+   */
+  @VisibleForTesting
+  static void unregister(SpanExporter spanExporter) {
+    StackdriverTraceExporter.unregister(spanExporter);
+  }
+
+  private StackdriverExporter() {}
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java
new file mode 100644
index 0000000..f78832d
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018, 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.exporter.trace.stackdriver;
+
+import com.google.auth.Credentials;
+import com.google.auto.value.AutoValue;
+import com.google.cloud.trace.v2.stub.TraceServiceStub;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link StackdriverTraceExporter}.
+ *
+ * @since 0.12
+ */
+@AutoValue
+@Immutable
+public abstract class StackdriverTraceConfiguration {
+
+  StackdriverTraceConfiguration() {}
+
+  /**
+   * Returns the {@link Credentials}.
+   *
+   * @return the {@code Credentials}.
+   * @since 0.12
+   */
+  @Nullable
+  public abstract Credentials getCredentials();
+
+  /**
+   * Returns the cloud project id.
+   *
+   * @return the cloud project id.
+   * @since 0.12
+   */
+  @Nullable
+  public abstract String getProjectId();
+
+  /**
+   * Returns a TraceServiceStub instance used to make RPC calls.
+   *
+   * @return the trace service stub.
+   * @since 0.16
+   */
+  @Nullable
+  public abstract TraceServiceStub getTraceServiceStub();
+
+  /**
+   * Returns a new {@link Builder}.
+   *
+   * @return a {@code Builder}.
+   * @since 0.12
+   */
+  public static Builder builder() {
+    return new AutoValue_StackdriverTraceConfiguration.Builder();
+  }
+
+  /**
+   * Builder for {@link StackdriverTraceConfiguration}.
+   *
+   * @since 0.12
+   */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    Builder() {}
+
+    /**
+     * Sets the {@link Credentials} used to authenticate API calls.
+     *
+     * @param credentials the {@code Credentials}.
+     * @return this.
+     * @since 0.12
+     */
+    public abstract Builder setCredentials(Credentials credentials);
+
+    /**
+     * Sets the cloud project id.
+     *
+     * @param projectId the cloud project id.
+     * @return this.
+     * @since 0.12
+     */
+    public abstract Builder setProjectId(String projectId);
+
+    /**
+     * Sets the trace service stub used to send gRPC calls.
+     *
+     * @param traceServiceStub the {@code TraceServiceStub}.
+     * @return this.
+     * @since 0.16
+     */
+    public abstract Builder setTraceServiceStub(TraceServiceStub traceServiceStub);
+
+    /**
+     * Builds a {@link StackdriverTraceConfiguration}.
+     *
+     * @return a {@code StackdriverTraceConfiguration}.
+     * @since 0.12
+     */
+    public abstract StackdriverTraceConfiguration build();
+  }
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java
new file mode 100644
index 0000000..0182ae9
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java
@@ -0,0 +1,141 @@
+/*
+ * 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.exporter.trace.stackdriver;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.trace.v2.TraceServiceClient;
+import com.google.cloud.trace.v2.stub.TraceServiceStub;
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.io.IOException;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Stackdriver Trace.
+ *
+ * <p>Example of usage on Google Cloud VMs:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ *   StackdriverTraceExporter.createAndRegister(
+ *       StackdriverTraceConfiguration.builder()
+ *           .setProjectId("MyStackdriverProjectId")
+ *           .build());
+ *   ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+public final class StackdriverTraceExporter {
+
+  private static final String REGISTER_NAME = StackdriverTraceExporter.class.getName();
+  private static final Object monitor = new Object();
+
+  @GuardedBy("monitor")
+  @Nullable
+  private static Handler handler = null;
+
+  /**
+   * Creates and registers the Stackdriver Trace exporter to the OpenCensus library. Only one
+   * Stackdriver exporter can be registered at any point.
+   *
+   * <p>If the {@code credentials} in the provided {@link StackdriverTraceConfiguration} is not set,
+   * the exporter will use the default application credentials. See {@link
+   * GoogleCredentials#getApplicationDefault}.
+   *
+   * <p>If the {@code projectId} in the provided {@link StackdriverTraceConfiguration} is not set,
+   * the exporter will use the default project ID. See {@link ServiceOptions#getDefaultProjectId}.
+   *
+   * @param configuration the {@code StackdriverTraceConfiguration} used to create the exporter.
+   * @throws IllegalStateException if a Stackdriver exporter is already registered.
+   * @since 0.12
+   */
+  public static void createAndRegister(StackdriverTraceConfiguration configuration)
+      throws IOException {
+    synchronized (monitor) {
+      checkState(handler == null, "Stackdriver exporter is already registered.");
+      Credentials credentials = configuration.getCredentials();
+      String projectId = configuration.getProjectId();
+      projectId = projectId != null ? projectId : ServiceOptions.getDefaultProjectId();
+
+      StackdriverV2ExporterHandler handler;
+      TraceServiceStub stub = configuration.getTraceServiceStub();
+      if (stub == null) {
+        handler =
+            StackdriverV2ExporterHandler.createWithCredentials(
+                credentials != null ? credentials : GoogleCredentials.getApplicationDefault(),
+                projectId);
+      } else {
+        handler = new StackdriverV2ExporterHandler(projectId, TraceServiceClient.create(stub));
+      }
+
+      registerInternal(handler);
+    }
+  }
+
+  private static void registerInternal(Handler newHandler) {
+    synchronized (monitor) {
+      handler = newHandler;
+      register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+    }
+  }
+
+  /**
+   * Registers the {@code StackdriverTraceExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+   */
+  @VisibleForTesting
+  static void register(SpanExporter spanExporter, Handler handler) {
+    spanExporter.registerHandler(REGISTER_NAME, handler);
+  }
+
+  /**
+   * Unregisters the Stackdriver Trace exporter from the OpenCensus library.
+   *
+   * @throws IllegalStateException if a Stackdriver exporter is not registered.
+   * @since 0.12
+   */
+  public static void unregister() {
+    synchronized (monitor) {
+      checkState(handler != null, "Stackdriver exporter is not registered.");
+      unregister(Tracing.getExportComponent().getSpanExporter());
+      handler = null;
+    }
+  }
+
+  /**
+   * Unregisters the {@code StackdriverTraceExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+   *     unregistered.
+   */
+  @VisibleForTesting
+  static void unregister(SpanExporter spanExporter) {
+    spanExporter.unregisterHandler(REGISTER_NAME);
+  }
+
+  private StackdriverTraceExporter() {}
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java
new file mode 100644
index 0000000..de022c3
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java
@@ -0,0 +1,501 @@
+/*
+ * 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.exporter.trace.stackdriver;
+
+import static com.google.api.client.util.Preconditions.checkNotNull;
+
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.auth.Credentials;
+import com.google.cloud.trace.v2.TraceServiceClient;
+import com.google.cloud.trace.v2.TraceServiceSettings;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.cloudtrace.v2.AttributeValue;
+import com.google.devtools.cloudtrace.v2.AttributeValue.Builder;
+import com.google.devtools.cloudtrace.v2.ProjectName;
+import com.google.devtools.cloudtrace.v2.Span;
+import com.google.devtools.cloudtrace.v2.Span.Attributes;
+import com.google.devtools.cloudtrace.v2.Span.Link;
+import com.google.devtools.cloudtrace.v2.Span.Links;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent.MessageEvent;
+import com.google.devtools.cloudtrace.v2.SpanName;
+import com.google.devtools.cloudtrace.v2.TruncatableString;
+import com.google.protobuf.Int32Value;
+import com.google.rpc.Status;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.OpenCensusLibraryInformation;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResourceUtils;
+import io.opencensus.contrib.monitoredresource.util.ResourceType;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Exporter to Stackdriver Trace API v2. */
+final class StackdriverV2ExporterHandler extends SpanExporter.Handler {
+
+  private static final Tracer tracer = Tracing.getTracer();
+  private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+  private static final String AGENT_LABEL_KEY = "g.co/agent";
+  private static final String AGENT_LABEL_VALUE_STRING =
+      "opencensus-java [" + OpenCensusLibraryInformation.VERSION + "]";
+  private static final String SERVER_PREFIX = "Recv.";
+  private static final String CLIENT_PREFIX = "Sent.";
+  private static final AttributeValue AGENT_LABEL_VALUE =
+      AttributeValue.newBuilder()
+          .setStringValue(toTruncatableStringProto(AGENT_LABEL_VALUE_STRING))
+          .build();
+
+  private static final ImmutableMap<String, String> HTTP_ATTRIBUTE_MAPPING =
+      ImmutableMap.<String, String>builder()
+          .put("http.host", "/http/host")
+          .put("http.method", "/http/method")
+          .put("http.path", "/http/path")
+          .put("http.route", "/http/route")
+          .put("http.user_agent", "/http/user_agent")
+          .put("http.status_code", "/http/status_code")
+          .build();
+
+  @javax.annotation.Nullable
+  private static final MonitoredResource RESOURCE = MonitoredResourceUtils.getDefaultResource();
+
+  // Only initialize once.
+  private static final Map<String, AttributeValue> RESOURCE_LABELS = getResourceLabels(RESOURCE);
+
+  // Constant functions for AttributeValue.
+  private static final Function<String, /*@Nullable*/ AttributeValue> stringAttributeValueFunction =
+      new Function<String, /*@Nullable*/ AttributeValue>() {
+        @Override
+        public AttributeValue apply(String stringValue) {
+          Builder attributeValueBuilder = AttributeValue.newBuilder();
+          attributeValueBuilder.setStringValue(toTruncatableStringProto(stringValue));
+          return attributeValueBuilder.build();
+        }
+      };
+  private static final Function<Boolean, /*@Nullable*/ AttributeValue>
+      booleanAttributeValueFunction =
+          new Function<Boolean, /*@Nullable*/ AttributeValue>() {
+            @Override
+            public AttributeValue apply(Boolean booleanValue) {
+              Builder attributeValueBuilder = AttributeValue.newBuilder();
+              attributeValueBuilder.setBoolValue(booleanValue);
+              return attributeValueBuilder.build();
+            }
+          };
+  private static final Function<Long, /*@Nullable*/ AttributeValue> longAttributeValueFunction =
+      new Function<Long, /*@Nullable*/ AttributeValue>() {
+        @Override
+        public AttributeValue apply(Long longValue) {
+          Builder attributeValueBuilder = AttributeValue.newBuilder();
+          attributeValueBuilder.setIntValue(longValue);
+          return attributeValueBuilder.build();
+        }
+      };
+  private static final Function<Double, /*@Nullable*/ AttributeValue> doubleAttributeValueFunction =
+      new Function<Double, /*@Nullable*/ AttributeValue>() {
+        @Override
+        public AttributeValue apply(Double doubleValue) {
+          Builder attributeValueBuilder = AttributeValue.newBuilder();
+          // TODO: set double value if Stackdriver Trace support it in the future.
+          attributeValueBuilder.setStringValue(
+              toTruncatableStringProto(String.valueOf(doubleValue)));
+          return attributeValueBuilder.build();
+        }
+      };
+
+  private final String projectId;
+  private final TraceServiceClient traceServiceClient;
+  private final ProjectName projectName;
+
+  @VisibleForTesting
+  StackdriverV2ExporterHandler(String projectId, TraceServiceClient traceServiceClient) {
+    this.projectId = checkNotNull(projectId, "projectId");
+    this.traceServiceClient = traceServiceClient;
+    projectName = ProjectName.of(this.projectId);
+
+    Tracing.getExportComponent()
+        .getSampledSpanStore()
+        .registerSpanNamesForCollection(Collections.singletonList("ExportStackdriverTraces"));
+  }
+
+  static StackdriverV2ExporterHandler createWithCredentials(
+      Credentials credentials, String projectId) throws IOException {
+    checkNotNull(credentials, "credentials");
+    TraceServiceSettings traceServiceSettings =
+        TraceServiceSettings.newBuilder()
+            .setCredentialsProvider(FixedCredentialsProvider.create(credentials))
+            .build();
+    return new StackdriverV2ExporterHandler(
+        projectId, TraceServiceClient.create(traceServiceSettings));
+  }
+
+  @VisibleForTesting
+  Span generateSpan(SpanData spanData, Map<String, AttributeValue> resourceLabels) {
+    SpanContext context = spanData.getContext();
+    final String spanIdHex = context.getSpanId().toLowerBase16();
+    SpanName spanName =
+        SpanName.newBuilder()
+            .setProject(projectId)
+            .setTrace(context.getTraceId().toLowerBase16())
+            .setSpan(spanIdHex)
+            .build();
+    Span.Builder spanBuilder =
+        Span.newBuilder()
+            .setName(spanName.toString())
+            .setSpanId(spanIdHex)
+            .setDisplayName(
+                toTruncatableStringProto(toDisplayName(spanData.getName(), spanData.getKind())))
+            .setStartTime(toTimestampProto(spanData.getStartTimestamp()))
+            .setAttributes(toAttributesProto(spanData.getAttributes(), resourceLabels))
+            .setTimeEvents(
+                toTimeEventsProto(spanData.getAnnotations(), spanData.getMessageEvents()));
+    io.opencensus.trace.Status status = spanData.getStatus();
+    if (status != null) {
+      spanBuilder.setStatus(toStatusProto(status));
+    }
+    Timestamp end = spanData.getEndTimestamp();
+    if (end != null) {
+      spanBuilder.setEndTime(toTimestampProto(end));
+    }
+    spanBuilder.setLinks(toLinksProto(spanData.getLinks()));
+    Integer childSpanCount = spanData.getChildSpanCount();
+    if (childSpanCount != null) {
+      spanBuilder.setChildSpanCount(Int32Value.newBuilder().setValue(childSpanCount).build());
+    }
+    if (spanData.getParentSpanId() != null && spanData.getParentSpanId().isValid()) {
+      spanBuilder.setParentSpanId(spanData.getParentSpanId().toLowerBase16());
+    }
+
+    return spanBuilder.build();
+  }
+
+  private static Span.TimeEvents toTimeEventsProto(
+      TimedEvents<Annotation> annotationTimedEvents,
+      TimedEvents<io.opencensus.trace.MessageEvent> messageEventTimedEvents) {
+    Span.TimeEvents.Builder timeEventsBuilder = Span.TimeEvents.newBuilder();
+    timeEventsBuilder.setDroppedAnnotationsCount(annotationTimedEvents.getDroppedEventsCount());
+    for (TimedEvent<Annotation> annotation : annotationTimedEvents.getEvents()) {
+      timeEventsBuilder.addTimeEvent(toTimeAnnotationProto(annotation));
+    }
+    timeEventsBuilder.setDroppedMessageEventsCount(messageEventTimedEvents.getDroppedEventsCount());
+    for (TimedEvent<io.opencensus.trace.MessageEvent> networkEvent :
+        messageEventTimedEvents.getEvents()) {
+      timeEventsBuilder.addTimeEvent(toTimeMessageEventProto(networkEvent));
+    }
+    return timeEventsBuilder.build();
+  }
+
+  private static TimeEvent toTimeAnnotationProto(TimedEvent<Annotation> timedEvent) {
+    TimeEvent.Builder timeEventBuilder =
+        TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+    Annotation annotation = timedEvent.getEvent();
+    timeEventBuilder.setAnnotation(
+        TimeEvent.Annotation.newBuilder()
+            .setDescription(toTruncatableStringProto(annotation.getDescription()))
+            .setAttributes(toAttributesBuilderProto(annotation.getAttributes(), 0))
+            .build());
+    return timeEventBuilder.build();
+  }
+
+  private static TimeEvent toTimeMessageEventProto(
+      TimedEvent<io.opencensus.trace.MessageEvent> timedEvent) {
+    TimeEvent.Builder timeEventBuilder =
+        TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+    io.opencensus.trace.MessageEvent messageEvent = timedEvent.getEvent();
+    timeEventBuilder.setMessageEvent(
+        TimeEvent.MessageEvent.newBuilder()
+            .setId(messageEvent.getMessageId())
+            .setCompressedSizeBytes(messageEvent.getCompressedMessageSize())
+            .setUncompressedSizeBytes(messageEvent.getUncompressedMessageSize())
+            .setType(toMessageEventTypeProto(messageEvent))
+            .build());
+    return timeEventBuilder.build();
+  }
+
+  private static TimeEvent.MessageEvent.Type toMessageEventTypeProto(
+      io.opencensus.trace.MessageEvent messageEvent) {
+    if (messageEvent.getType() == Type.RECEIVED) {
+      return MessageEvent.Type.RECEIVED;
+    } else {
+      return MessageEvent.Type.SENT;
+    }
+  }
+
+  // These are the attributes of the Span, where usually we may add more attributes like the agent.
+  private static Attributes toAttributesProto(
+      io.opencensus.trace.export.SpanData.Attributes attributes,
+      Map<String, AttributeValue> resourceLabels) {
+    Attributes.Builder attributesBuilder =
+        toAttributesBuilderProto(
+            attributes.getAttributeMap(), attributes.getDroppedAttributesCount());
+    attributesBuilder.putAttributeMap(AGENT_LABEL_KEY, AGENT_LABEL_VALUE);
+    for (Entry<String, AttributeValue> entry : resourceLabels.entrySet()) {
+      attributesBuilder.putAttributeMap(entry.getKey(), entry.getValue());
+    }
+    return attributesBuilder.build();
+  }
+
+  private static Attributes.Builder toAttributesBuilderProto(
+      Map<String, io.opencensus.trace.AttributeValue> attributes, int droppedAttributesCount) {
+    Attributes.Builder attributesBuilder =
+        Attributes.newBuilder().setDroppedAttributesCount(droppedAttributesCount);
+    for (Map.Entry<String, io.opencensus.trace.AttributeValue> label : attributes.entrySet()) {
+      AttributeValue value = toAttributeValueProto(label.getValue());
+      if (value != null) {
+        attributesBuilder.putAttributeMap(mapKey(label.getKey()), value);
+      }
+    }
+    return attributesBuilder;
+  }
+
+  @VisibleForTesting
+  static Map<String, AttributeValue> getResourceLabels(
+      @javax.annotation.Nullable MonitoredResource resource) {
+    if (resource == null) {
+      return Collections.emptyMap();
+    }
+    Map<String, AttributeValue> resourceLabels = new HashMap<String, AttributeValue>();
+    ResourceType resourceType = resource.getResourceType();
+    switch (resourceType) {
+      case AWS_EC2_INSTANCE:
+        AwsEc2InstanceMonitoredResource awsEc2InstanceMonitoredResource =
+            (AwsEc2InstanceMonitoredResource) resource;
+        putToResourceAttributeMap(
+            resourceLabels,
+            resourceType,
+            "aws_account",
+            awsEc2InstanceMonitoredResource.getAccount());
+        putToResourceAttributeMap(
+            resourceLabels,
+            resourceType,
+            "instance_id",
+            awsEc2InstanceMonitoredResource.getInstanceId());
+        putToResourceAttributeMap(
+            resourceLabels,
+            resourceType,
+            "region",
+            "aws:" + awsEc2InstanceMonitoredResource.getRegion());
+        return Collections.unmodifiableMap(resourceLabels);
+      case GCP_GCE_INSTANCE:
+        GcpGceInstanceMonitoredResource gcpGceInstanceMonitoredResource =
+            (GcpGceInstanceMonitoredResource) resource;
+        putToResourceAttributeMap(
+            resourceLabels,
+            resourceType,
+            "project_id",
+            gcpGceInstanceMonitoredResource.getAccount());
+        putToResourceAttributeMap(
+            resourceLabels,
+            resourceType,
+            "instance_id",
+            gcpGceInstanceMonitoredResource.getInstanceId());
+        putToResourceAttributeMap(
+            resourceLabels, resourceType, "zone", gcpGceInstanceMonitoredResource.getZone());
+        return Collections.unmodifiableMap(resourceLabels);
+      case GCP_GKE_CONTAINER:
+        GcpGkeContainerMonitoredResource gcpGkeContainerMonitoredResource =
+            (GcpGkeContainerMonitoredResource) resource;
+        putToResourceAttributeMap(
+            resourceLabels,
+            resourceType,
+            "project_id",
+            gcpGkeContainerMonitoredResource.getAccount());
+        putToResourceAttributeMap(
+            resourceLabels, resourceType, "location", gcpGkeContainerMonitoredResource.getZone());
+        putToResourceAttributeMap(
+            resourceLabels,
+            resourceType,
+            "cluster_name",
+            gcpGkeContainerMonitoredResource.getClusterName());
+        putToResourceAttributeMap(
+            resourceLabels,
+            resourceType,
+            "container_name",
+            gcpGkeContainerMonitoredResource.getContainerName());
+        putToResourceAttributeMap(
+            resourceLabels,
+            resourceType,
+            "namespace_name",
+            gcpGkeContainerMonitoredResource.getNamespaceId());
+        putToResourceAttributeMap(
+            resourceLabels, resourceType, "pod_name", gcpGkeContainerMonitoredResource.getPodId());
+        return Collections.unmodifiableMap(resourceLabels);
+    }
+    return Collections.emptyMap();
+  }
+
+  private static void putToResourceAttributeMap(
+      Map<String, AttributeValue> map,
+      ResourceType resourceType,
+      String attributeName,
+      String attributeValue) {
+    map.put(
+        createResourceLabelKey(resourceType, attributeName),
+        toStringAttributeValueProto(attributeValue));
+  }
+
+  @VisibleForTesting
+  static String createResourceLabelKey(ResourceType resourceType, String resourceAttribute) {
+    return String.format("g.co/r/%s/%s", mapToStringResourceType(resourceType), resourceAttribute);
+  }
+
+  private static String mapToStringResourceType(ResourceType resourceType) {
+    switch (resourceType) {
+      case GCP_GCE_INSTANCE:
+        return "gce_instance";
+      case GCP_GKE_CONTAINER:
+        return "k8s_container";
+      case AWS_EC2_INSTANCE:
+        return "aws_ec2_instance";
+    }
+    throw new IllegalArgumentException("Unknown resource type.");
+  }
+
+  @VisibleForTesting
+  static AttributeValue toStringAttributeValueProto(String value) {
+    return AttributeValue.newBuilder().setStringValue(toTruncatableStringProto(value)).build();
+  }
+
+  private static String mapKey(String key) {
+    if (HTTP_ATTRIBUTE_MAPPING.containsKey(key)) {
+      return HTTP_ATTRIBUTE_MAPPING.get(key);
+    } else {
+      return key;
+    }
+  }
+
+  private static Status toStatusProto(io.opencensus.trace.Status status) {
+    Status.Builder statusBuilder = Status.newBuilder().setCode(status.getCanonicalCode().value());
+    if (status.getDescription() != null) {
+      statusBuilder.setMessage(status.getDescription());
+    }
+    return statusBuilder.build();
+  }
+
+  private static TruncatableString toTruncatableStringProto(String string) {
+    return TruncatableString.newBuilder().setValue(string).setTruncatedByteCount(0).build();
+  }
+
+  private static com.google.protobuf.Timestamp toTimestampProto(Timestamp timestamp) {
+    return com.google.protobuf.Timestamp.newBuilder()
+        .setSeconds(timestamp.getSeconds())
+        .setNanos(timestamp.getNanos())
+        .build();
+  }
+
+  @javax.annotation.Nullable
+  private static AttributeValue toAttributeValueProto(
+      io.opencensus.trace.AttributeValue attributeValue) {
+    return attributeValue.match(
+        stringAttributeValueFunction,
+        booleanAttributeValueFunction,
+        longAttributeValueFunction,
+        doubleAttributeValueFunction,
+        Functions.</*@Nullable*/ AttributeValue>returnNull());
+  }
+
+  private static Link.Type toLinkTypeProto(io.opencensus.trace.Link.Type type) {
+    if (type == io.opencensus.trace.Link.Type.PARENT_LINKED_SPAN) {
+      return Link.Type.PARENT_LINKED_SPAN;
+    } else {
+      return Link.Type.CHILD_LINKED_SPAN;
+    }
+  }
+
+  private static String toDisplayName(String spanName, @javax.annotation.Nullable Kind spanKind) {
+    if (spanKind == Kind.SERVER && !spanName.startsWith(SERVER_PREFIX)) {
+      return SERVER_PREFIX + spanName;
+    }
+
+    if (spanKind == Kind.CLIENT && !spanName.startsWith(CLIENT_PREFIX)) {
+      return CLIENT_PREFIX + spanName;
+    }
+
+    return spanName;
+  }
+
+  private static Link toLinkProto(io.opencensus.trace.Link link) {
+    checkNotNull(link);
+    return Link.newBuilder()
+        .setTraceId(link.getTraceId().toLowerBase16())
+        .setSpanId(link.getSpanId().toLowerBase16())
+        .setType(toLinkTypeProto(link.getType()))
+        .setAttributes(toAttributesBuilderProto(link.getAttributes(), 0))
+        .build();
+  }
+
+  private static Links toLinksProto(io.opencensus.trace.export.SpanData.Links links) {
+    final Links.Builder linksBuilder =
+        Links.newBuilder().setDroppedLinksCount(links.getDroppedLinksCount());
+    for (io.opencensus.trace.Link link : links.getLinks()) {
+      linksBuilder.addLink(toLinkProto(link));
+    }
+    return linksBuilder.build();
+  }
+
+  @Override
+  public void export(Collection<SpanData> spanDataList) {
+    // Start a new span with explicit 1/10000 sampling probability to avoid the case when user
+    // sets the default sampler to always sample and we get the gRPC span of the stackdriver
+    // export call always sampled and go to an infinite loop.
+    Scope scope =
+        tracer
+            .spanBuilder("ExportStackdriverTraces")
+            .setSampler(probabilitySampler)
+            .setRecordEvents(true)
+            .startScopedSpan();
+    try {
+      List<Span> spans = new ArrayList<>(spanDataList.size());
+      for (SpanData spanData : spanDataList) {
+        spans.add(generateSpan(spanData, RESOURCE_LABELS));
+      }
+      // Sync call because it is already called for a batch of data, and on a separate thread.
+      // TODO(bdrutu): Consider to make this async in the future.
+      traceServiceClient.batchWriteSpans(projectName, spans);
+    } finally {
+      scope.close();
+    }
+  }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java
new file mode 100644
index 0000000..6926e86
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.exporter.trace.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import java.util.Date;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverTraceConfiguration}. */
+@RunWith(JUnit4.class)
+public class StackdriverTraceConfigurationTest {
+
+  private static final Credentials FAKE_CREDENTIALS =
+      GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+  private static final String PROJECT_ID = "project";
+
+  @Test
+  public void defaultConfiguration() {
+    StackdriverTraceConfiguration configuration = StackdriverTraceConfiguration.builder().build();
+    assertThat(configuration.getCredentials()).isNull();
+    assertThat(configuration.getProjectId()).isNull();
+  }
+
+  @Test
+  public void updateAll() {
+    StackdriverTraceConfiguration configuration =
+        StackdriverTraceConfiguration.builder()
+            .setCredentials(FAKE_CREDENTIALS)
+            .setProjectId(PROJECT_ID)
+            .build();
+    assertThat(configuration.getCredentials()).isEqualTo(FAKE_CREDENTIALS);
+    assertThat(configuration.getProjectId()).isEqualTo(PROJECT_ID);
+  }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java
new file mode 100644
index 0000000..6a12a89
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.exporter.trace.stackdriver;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link StackdriverTraceExporter}. */
+@RunWith(JUnit4.class)
+public class StackdriverTraceExporterTest {
+  @Mock private SpanExporter spanExporter;
+  @Mock private Handler handler;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void registerUnregisterStackdriverExporter() {
+    StackdriverTraceExporter.register(spanExporter, handler);
+    verify(spanExporter)
+        .registerHandler(
+            eq("io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter"), same(handler));
+    StackdriverTraceExporter.unregister(spanExporter);
+    verify(spanExporter)
+        .unregisterHandler(eq("io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter"));
+  }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java
new file mode 100644
index 0000000..3245859
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018, 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.exporter.trace.stackdriver;
+
+import static org.mockito.Mockito.when;
+
+import com.google.cloud.trace.v2.TraceServiceClient;
+import com.google.cloud.trace.v2.stub.TraceServiceStub;
+import io.opencensus.trace.export.SpanData;
+import java.util.Collection;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for exporting in {@link StackdriverV2ExporterHandler}. */
+@RunWith(JUnit4.class)
+public final class StackdriverV2ExporterHandlerExportTest {
+  private static final String PROJECT_ID = "PROJECT_ID";
+  // mock the service stub to provide a fake trace service.
+  @Mock private TraceServiceStub traceServiceStub;
+  private TraceServiceClient traceServiceClient;
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private StackdriverV2ExporterHandler handler;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    // TODO(@Hailong): TraceServiceClient.create(TraceServiceStub) is a beta API and might change
+    // in the future.
+    traceServiceClient = TraceServiceClient.create(traceServiceStub);
+    handler = new StackdriverV2ExporterHandler(PROJECT_ID, traceServiceClient);
+  }
+
+  @Test
+  public void export() {
+    when(traceServiceStub.batchWriteSpansCallable())
+        .thenThrow(new RuntimeException("TraceServiceStub called"));
+    Collection<SpanData> spanDataList = Collections.<SpanData>emptyList();
+    thrown.expect(RuntimeException.class);
+    thrown.expectMessage("TraceServiceStub called");
+    handler.export(spanDataList);
+  }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java
new file mode 100644
index 0000000..8b28dc0
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright 2018, 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.exporter.trace.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.monitoredresource.util.ResourceType.AWS_EC2_INSTANCE;
+import static io.opencensus.contrib.monitoredresource.util.ResourceType.GCP_GCE_INSTANCE;
+import static io.opencensus.contrib.monitoredresource.util.ResourceType.GCP_GKE_CONTAINER;
+import static io.opencensus.exporter.trace.stackdriver.StackdriverV2ExporterHandler.createResourceLabelKey;
+import static io.opencensus.exporter.trace.stackdriver.StackdriverV2ExporterHandler.toStringAttributeValueProto;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.cloudtrace.v2.AttributeValue;
+import com.google.devtools.cloudtrace.v2.Span;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent.MessageEvent;
+import com.google.devtools.cloudtrace.v2.StackTrace;
+import com.google.devtools.cloudtrace.v2.TruncatableString;
+import com.google.protobuf.Int32Value;
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for proto conversions in {@link StackdriverV2ExporterHandler}. */
+@RunWith(JUnit4.class)
+public final class StackdriverV2ExporterHandlerProtoTest {
+
+  private static final Credentials FAKE_CREDENTIALS =
+      GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+  // OpenCensus constants
+  private static final Timestamp startTimestamp = Timestamp.create(123, 456);
+  private static final Timestamp eventTimestamp1 = Timestamp.create(123, 457);
+  private static final Timestamp eventTimestamp2 = Timestamp.create(123, 458);
+  private static final Timestamp eventTimestamp3 = Timestamp.create(123, 459);
+  private static final Timestamp endTimestamp = Timestamp.create(123, 460);
+
+  private static final String PROJECT_ID = "PROJECT_ID";
+  private static final String TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736";
+  private static final String SPAN_ID = "24aa0b2d371f48c9";
+  private static final String PARENT_SPAN_ID = "71da8d631536f5f1";
+  private static final String SPAN_NAME = "MySpanName";
+  private static final String SD_SPAN_NAME =
+      String.format("projects/%s/traces/%s/spans/%s", PROJECT_ID, TRACE_ID, SPAN_ID);
+  private static final String ANNOTATION_TEXT = "MyAnnotationText";
+  private static final String ATTRIBUTE_KEY_1 = "MyAttributeKey1";
+  private static final String ATTRIBUTE_KEY_2 = "MyAttributeKey2";
+
+  private static final int DROPPED_ATTRIBUTES_COUNT = 1;
+  private static final int DROPPED_ANNOTATIONS_COUNT = 2;
+  private static final int DROPPED_NETWORKEVENTS_COUNT = 3;
+  private static final int DROPPED_LINKS_COUNT = 4;
+  private static final int CHILD_SPAN_COUNT = 13;
+
+  private static final Annotation annotation = Annotation.fromDescription(ANNOTATION_TEXT);
+  private static final io.opencensus.trace.MessageEvent recvMessageEvent =
+      io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.RECEIVED, 1)
+          .build();
+  private static final io.opencensus.trace.MessageEvent sentMessageEvent =
+      io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.SENT, 1)
+          .build();
+  private static final Status status = Status.DEADLINE_EXCEEDED.withDescription("TooSlow");
+  private static final SpanId parentSpanId = SpanId.fromLowerBase16(PARENT_SPAN_ID);
+  private static final SpanId spanId = SpanId.fromLowerBase16(SPAN_ID);
+  private static final TraceId traceId = TraceId.fromLowerBase16(TRACE_ID);
+  private static final TraceOptions traceOptions = TraceOptions.DEFAULT;
+  private static final SpanContext spanContext = SpanContext.create(traceId, spanId, traceOptions);
+
+  private static final List<TimedEvent<Annotation>> annotationsList =
+      ImmutableList.of(
+          SpanData.TimedEvent.create(eventTimestamp1, annotation),
+          SpanData.TimedEvent.create(eventTimestamp3, annotation));
+  private static final List<TimedEvent<io.opencensus.trace.MessageEvent>> networkEventsList =
+      ImmutableList.of(
+          SpanData.TimedEvent.create(eventTimestamp1, recvMessageEvent),
+          SpanData.TimedEvent.create(eventTimestamp2, sentMessageEvent));
+  private static final List<Link> linksList =
+      ImmutableList.of(Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN));
+
+  private static final SpanData.Attributes attributes =
+      SpanData.Attributes.create(
+          ImmutableMap.of(
+              ATTRIBUTE_KEY_1,
+              io.opencensus.trace.AttributeValue.longAttributeValue(10L),
+              ATTRIBUTE_KEY_2,
+              io.opencensus.trace.AttributeValue.booleanAttributeValue(true)),
+          DROPPED_ATTRIBUTES_COUNT);
+  private static final TimedEvents<Annotation> annotations =
+      TimedEvents.create(annotationsList, DROPPED_ANNOTATIONS_COUNT);
+  private static final TimedEvents<io.opencensus.trace.MessageEvent> messageEvents =
+      TimedEvents.create(networkEventsList, DROPPED_NETWORKEVENTS_COUNT);
+  private static final SpanData.Links links = SpanData.Links.create(linksList, DROPPED_LINKS_COUNT);
+  private static final Map<String, AttributeValue> EMPTY_RESOURCE_LABELS = Collections.emptyMap();
+  private static final AwsEc2InstanceMonitoredResource AWS_EC2_INSTANCE_MONITORED_RESOURCE =
+      AwsEc2InstanceMonitoredResource.create("my-project", "my-instance", "us-east-1");
+  private static final GcpGceInstanceMonitoredResource GCP_GCE_INSTANCE_MONITORED_RESOURCE =
+      GcpGceInstanceMonitoredResource.create("my-project", "my-instance", "us-east1");
+  private static final GcpGkeContainerMonitoredResource GCP_GKE_CONTAINER_MONITORED_RESOURCE =
+      GcpGkeContainerMonitoredResource.create(
+          "my-project", "cluster", "container", "namespace", "my-instance", "pod", "us-east1");
+  private static final ImmutableMap<String, AttributeValue> AWS_RESOURCE_LABELS =
+      ImmutableMap.of(
+          createResourceLabelKey(AWS_EC2_INSTANCE, "aws_account"),
+          toStringAttributeValueProto("my-project"),
+          createResourceLabelKey(AWS_EC2_INSTANCE, "instance_id"),
+          toStringAttributeValueProto("my-instance"),
+          createResourceLabelKey(AWS_EC2_INSTANCE, "region"),
+          toStringAttributeValueProto("aws:us-east-1"));
+  private static final ImmutableMap<String, AttributeValue> GCE_RESOURCE_LABELS =
+      ImmutableMap.of(
+          createResourceLabelKey(GCP_GCE_INSTANCE, "project_id"),
+          toStringAttributeValueProto("my-project"),
+          createResourceLabelKey(GCP_GCE_INSTANCE, "instance_id"),
+          toStringAttributeValueProto("my-instance"),
+          createResourceLabelKey(GCP_GCE_INSTANCE, "zone"),
+          toStringAttributeValueProto("us-east1"));
+  private static final ImmutableMap<String, AttributeValue> GKE_RESOURCE_LABELS =
+      ImmutableMap.<String, AttributeValue>builder()
+          .put(
+              createResourceLabelKey(GCP_GKE_CONTAINER, "project_id"),
+              toStringAttributeValueProto("my-project"))
+          .put(
+              createResourceLabelKey(GCP_GKE_CONTAINER, "cluster_name"),
+              toStringAttributeValueProto("cluster"))
+          .put(
+              createResourceLabelKey(GCP_GKE_CONTAINER, "container_name"),
+              toStringAttributeValueProto("container"))
+          .put(
+              createResourceLabelKey(GCP_GKE_CONTAINER, "namespace_name"),
+              toStringAttributeValueProto("namespace"))
+          .put(
+              createResourceLabelKey(GCP_GKE_CONTAINER, "pod_name"),
+              toStringAttributeValueProto("pod"))
+          .put(
+              createResourceLabelKey(GCP_GKE_CONTAINER, "location"),
+              toStringAttributeValueProto("us-east1"))
+          .build();
+
+  private StackdriverV2ExporterHandler handler;
+
+  @Before
+  public void setUp() throws IOException {
+    handler = StackdriverV2ExporterHandler.createWithCredentials(FAKE_CREDENTIALS, PROJECT_ID);
+  }
+
+  @Test
+  public void generateSpan() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            /* hasRemoteParent= */ true,
+            SPAN_NAME,
+            null,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    TimeEvent annotationTimeEvent1 =
+        TimeEvent.newBuilder()
+            .setAnnotation(
+                TimeEvent.Annotation.newBuilder()
+                    .setDescription(
+                        TruncatableString.newBuilder().setValue(ANNOTATION_TEXT).build())
+                    .setAttributes(Span.Attributes.newBuilder().build())
+                    .build())
+            .setTime(
+                com.google.protobuf.Timestamp.newBuilder()
+                    .setSeconds(eventTimestamp1.getSeconds())
+                    .setNanos(eventTimestamp1.getNanos())
+                    .build())
+            .build();
+    TimeEvent annotationTimeEvent2 =
+        TimeEvent.newBuilder()
+            .setAnnotation(
+                TimeEvent.Annotation.newBuilder()
+                    .setDescription(
+                        TruncatableString.newBuilder().setValue(ANNOTATION_TEXT).build())
+                    .setAttributes(Span.Attributes.newBuilder().build())
+                    .build())
+            .setTime(
+                com.google.protobuf.Timestamp.newBuilder()
+                    .setSeconds(eventTimestamp3.getSeconds())
+                    .setNanos(eventTimestamp3.getNanos())
+                    .build())
+            .build();
+
+    TimeEvent sentTimeEvent =
+        TimeEvent.newBuilder()
+            .setMessageEvent(
+                TimeEvent.MessageEvent.newBuilder()
+                    .setType(MessageEvent.Type.SENT)
+                    .setId(sentMessageEvent.getMessageId()))
+            .setTime(
+                com.google.protobuf.Timestamp.newBuilder()
+                    .setSeconds(eventTimestamp2.getSeconds())
+                    .setNanos(eventTimestamp2.getNanos())
+                    .build())
+            .build();
+    TimeEvent recvTimeEvent =
+        TimeEvent.newBuilder()
+            .setMessageEvent(
+                TimeEvent.MessageEvent.newBuilder()
+                    .setType(MessageEvent.Type.RECEIVED)
+                    .setId(recvMessageEvent.getMessageId()))
+            .setTime(
+                com.google.protobuf.Timestamp.newBuilder()
+                    .setSeconds(eventTimestamp1.getSeconds())
+                    .setNanos(eventTimestamp1.getNanos())
+                    .build())
+            .build();
+
+    Span.Links spanLinks =
+        Span.Links.newBuilder()
+            .setDroppedLinksCount(DROPPED_LINKS_COUNT)
+            .addLink(
+                Span.Link.newBuilder()
+                    .setType(Span.Link.Type.CHILD_LINKED_SPAN)
+                    .setTraceId(TRACE_ID)
+                    .setSpanId(SPAN_ID)
+                    .setAttributes(Span.Attributes.newBuilder().build())
+                    .build())
+            .build();
+
+    com.google.rpc.Status spanStatus =
+        com.google.rpc.Status.newBuilder()
+            .setCode(com.google.rpc.Code.DEADLINE_EXCEEDED.getNumber())
+            .setMessage("TooSlow")
+            .build();
+
+    com.google.protobuf.Timestamp startTime =
+        com.google.protobuf.Timestamp.newBuilder()
+            .setSeconds(startTimestamp.getSeconds())
+            .setNanos(startTimestamp.getNanos())
+            .build();
+    com.google.protobuf.Timestamp endTime =
+        com.google.protobuf.Timestamp.newBuilder()
+            .setSeconds(endTimestamp.getSeconds())
+            .setNanos(endTimestamp.getNanos())
+            .build();
+
+    Span span = handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS);
+    assertThat(span.getName()).isEqualTo(SD_SPAN_NAME);
+    assertThat(span.getSpanId()).isEqualTo(SPAN_ID);
+    assertThat(span.getParentSpanId()).isEqualTo(PARENT_SPAN_ID);
+    assertThat(span.getDisplayName())
+        .isEqualTo(TruncatableString.newBuilder().setValue(SPAN_NAME).build());
+    assertThat(span.getStartTime()).isEqualTo(startTime);
+    assertThat(span.getEndTime()).isEqualTo(endTime);
+    assertThat(span.getAttributes().getDroppedAttributesCount())
+        .isEqualTo(DROPPED_ATTRIBUTES_COUNT);
+    // The generated attributes map contains more values (e.g. agent). We only test what we added.
+    assertThat(span.getAttributes().getAttributeMapMap())
+        .containsEntry(ATTRIBUTE_KEY_1, AttributeValue.newBuilder().setIntValue(10L).build());
+    assertThat(span.getAttributes().getAttributeMapMap())
+        .containsEntry(ATTRIBUTE_KEY_2, AttributeValue.newBuilder().setBoolValue(true).build());
+    // TODO(@Hailong): add stack trace test in the future.
+    assertThat(span.getStackTrace()).isEqualTo(StackTrace.newBuilder().build());
+    assertThat(span.getTimeEvents().getDroppedMessageEventsCount())
+        .isEqualTo(DROPPED_NETWORKEVENTS_COUNT);
+    assertThat(span.getTimeEvents().getDroppedAnnotationsCount())
+        .isEqualTo(DROPPED_ANNOTATIONS_COUNT);
+    assertThat(span.getTimeEvents().getTimeEventList())
+        .containsAllOf(annotationTimeEvent1, annotationTimeEvent2, sentTimeEvent, recvTimeEvent);
+    assertThat(span.getLinks()).isEqualTo(spanLinks);
+    assertThat(span.getStatus()).isEqualTo(spanStatus);
+    assertThat(span.getSameProcessAsParentSpan())
+        .isEqualTo(com.google.protobuf.BoolValue.newBuilder().build());
+    assertThat(span.getChildSpanCount())
+        .isEqualTo(Int32Value.newBuilder().setValue(CHILD_SPAN_COUNT).build());
+  }
+
+  @Test
+  public void getResourceLabels_AwsEc2ResourceLabels() {
+    testGetResourceLabels(AWS_EC2_INSTANCE_MONITORED_RESOURCE, AWS_RESOURCE_LABELS);
+  }
+
+  @Test
+  public void getResourceLabels_GceResourceLabels() {
+    testGetResourceLabels(GCP_GCE_INSTANCE_MONITORED_RESOURCE, GCE_RESOURCE_LABELS);
+  }
+
+  @Test
+  public void getResourceLabels_GkeResourceLabels() {
+    testGetResourceLabels(GCP_GKE_CONTAINER_MONITORED_RESOURCE, GKE_RESOURCE_LABELS);
+  }
+
+  private static void testGetResourceLabels(
+      MonitoredResource resource, Map<String, AttributeValue> expectedLabels) {
+    Map<String, AttributeValue> actualLabels =
+        StackdriverV2ExporterHandler.getResourceLabels(resource);
+    assertThat(actualLabels).containsExactlyEntriesIn(expectedLabels);
+  }
+
+  @Test
+  public void generateSpan_WithResourceLabels() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            /* hasRemoteParent= */ true,
+            SPAN_NAME,
+            null,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    Span span = handler.generateSpan(spanData, AWS_RESOURCE_LABELS);
+    Map<String, AttributeValue> attributeMap = span.getAttributes().getAttributeMapMap();
+    assertThat(attributeMap.entrySet()).containsAllIn(AWS_RESOURCE_LABELS.entrySet());
+  }
+
+  @Test
+  public void mapHttpAttributes() {
+    Map<String, io.opencensus.trace.AttributeValue> attributesMap =
+        new HashMap<String, io.opencensus.trace.AttributeValue>();
+
+    attributesMap.put("http.host", io.opencensus.trace.AttributeValue.stringAttributeValue("host"));
+    attributesMap.put(
+        "http.method", io.opencensus.trace.AttributeValue.stringAttributeValue("method"));
+    attributesMap.put("http.path", io.opencensus.trace.AttributeValue.stringAttributeValue("path"));
+    attributesMap.put(
+        "http.route", io.opencensus.trace.AttributeValue.stringAttributeValue("route"));
+    attributesMap.put(
+        "http.user_agent", io.opencensus.trace.AttributeValue.stringAttributeValue("user_agent"));
+    attributesMap.put(
+        "http.status_code", io.opencensus.trace.AttributeValue.longAttributeValue(200L));
+    SpanData.Attributes httpAttributes = SpanData.Attributes.create(attributesMap, 0);
+
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            /* hasRemoteParent= */ true,
+            SPAN_NAME,
+            startTimestamp,
+            httpAttributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+
+    Span span = handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS);
+    Map<String, AttributeValue> attributes = span.getAttributes().getAttributeMapMap();
+
+    assertThat(attributes).containsEntry("/http/host", toStringAttributeValueProto("host"));
+    assertThat(attributes).containsEntry("/http/method", toStringAttributeValueProto("method"));
+    assertThat(attributes).containsEntry("/http/path", toStringAttributeValueProto("path"));
+    assertThat(attributes).containsEntry("/http/route", toStringAttributeValueProto("route"));
+    assertThat(attributes)
+        .containsEntry("/http/user_agent", toStringAttributeValueProto("user_agent"));
+    assertThat(attributes)
+        .containsEntry("/http/status_code", AttributeValue.newBuilder().setIntValue(200L).build());
+  }
+
+  @Test
+  public void generateSpanName_ForServer() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            /* hasRemoteParent= */ true,
+            SPAN_NAME,
+            Kind.SERVER,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+        .isEqualTo("Recv." + SPAN_NAME);
+  }
+
+  @Test
+  public void generateSpanName_ForServerWithRecv() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            /* hasRemoteParent= */ true,
+            "Recv." + SPAN_NAME,
+            Kind.SERVER,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+        .isEqualTo("Recv." + SPAN_NAME);
+  }
+
+  @Test
+  public void generateSpanName_ForClient() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            /* hasRemoteParent= */ true,
+            SPAN_NAME,
+            Kind.CLIENT,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+        .isEqualTo("Sent." + SPAN_NAME);
+  }
+
+  @Test
+  public void generateSpanName_ForClientWithSent() {
+    SpanData spanData =
+        SpanData.create(
+            spanContext,
+            parentSpanId,
+            /* hasRemoteParent= */ true,
+            "Sent." + SPAN_NAME,
+            Kind.CLIENT,
+            startTimestamp,
+            attributes,
+            annotations,
+            messageEvents,
+            links,
+            CHILD_SPAN_COUNT,
+            status,
+            endTimestamp);
+    assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+        .isEqualTo("Sent." + SPAN_NAME);
+  }
+}
diff --git a/exporters/trace/zipkin/README.md b/exporters/trace/zipkin/README.md
new file mode 100644
index 0000000..4398360
--- /dev/null
+++ b/exporters/trace/zipkin/README.md
@@ -0,0 +1,82 @@
+# OpenCensus Zipkin Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Zipkin Trace Exporter* is a trace exporter that exports
+data to Zipkin. [Zipkin](http://zipkin.io/) Zipkin is a distributed
+tracing system. It helps gather timing data needed to troubleshoot
+latency problems in microservice architectures. It manages both the
+collection and lookup of this data.
+
+## Quickstart
+
+### Prerequisites
+
+[Zipkin](http://zipkin.io/) stores and queries traces exported by
+applications instrumented with Census. The easiest way to start a zipkin
+server is to paste the below:
+
+```bash
+wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
+java -jar zipkin.jar
+```
+
+
+### Hello Zipkin
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-api</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-exporter-trace-zipkin</artifactId>
+    <version>0.16.1</version>
+  </dependency>
+  <dependency>
+    <groupId>io.opencensus</groupId>
+    <artifactId>opencensus-impl</artifactId>
+    <version>0.16.1</version>
+    <scope>runtime</scope>
+  </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-zipkin:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This will report Zipkin v2 json format to a single server. Alternate
+[senders](https://github.com/openzipkin/zipkin-reporter-java) are available.
+
+```java
+public class MyMainClass {
+  public static void main(String[] args) throws Exception {
+    ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "my-service");
+    // ...
+  }
+}
+```
+
+#### Java Versions
+
+Java 6 or above is required for using this exporter.
+
+[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-exporter-trace-zipkin/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-zipkin
diff --git a/exporters/trace/zipkin/build.gradle b/exporters/trace/zipkin/build.gradle
new file mode 100644
index 0000000..530dff7
--- /dev/null
+++ b/exporters/trace/zipkin/build.gradle
@@ -0,0 +1,18 @@
+description = 'OpenCensus Trace Zipkin Exporter'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.6
+    it.targetCompatibility = 1.6
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.guava,
+            libraries.zipkin_reporter,
+            libraries.zipkin_urlconnection
+
+    testCompile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java
new file mode 100644
index 0000000..e20360e
--- /dev/null
+++ b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java
@@ -0,0 +1,104 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import zipkin2.Span;
+import zipkin2.codec.SpanBytesEncoder;
+import zipkin2.reporter.Sender;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Zipkin.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ *   ZipkinExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "myservicename");
+ *   ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @deprecated Deprecated due to inconsistent naming. Use {@link ZipkinTraceExporter}.
+ * @since 0.8
+ */
+@Deprecated
+public final class ZipkinExporter {
+
+  private ZipkinExporter() {}
+
+  /**
+   * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+   * exporter can be registered at any point.
+   *
+   * @param v2Url Ex http://127.0.0.1:9411/api/v2/spans
+   * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+   * @throws IllegalStateException if a Zipkin exporter is already registered.
+   * @since 0.8
+   */
+  public static void createAndRegister(String v2Url, String serviceName) {
+    ZipkinTraceExporter.createAndRegister(v2Url, serviceName);
+  }
+
+  /**
+   * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+   * exporter can be registered at any point.
+   *
+   * @param encoder Usually {@link SpanBytesEncoder#JSON_V2}
+   * @param sender Often, but not necessarily an http sender. This could be Kafka or SQS.
+   * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+   * @throws IllegalStateException if a Zipkin exporter is already registered.
+   * @since 0.8
+   */
+  public static void createAndRegister(
+      SpanBytesEncoder encoder, Sender sender, String serviceName) {
+    ZipkinTraceExporter.createAndRegister(encoder, sender, serviceName);
+  }
+
+  /**
+   * Registers the {@code ZipkinExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+   */
+  @VisibleForTesting
+  static void register(SpanExporter spanExporter, Handler handler) {
+    ZipkinTraceExporter.register(spanExporter, handler);
+  }
+
+  /**
+   * Unregisters the Zipkin Trace exporter from the OpenCensus library.
+   *
+   * @throws IllegalStateException if a Zipkin exporter is not registered.
+   * @since 0.8
+   */
+  public static void unregister() {
+    ZipkinTraceExporter.unregister();
+  }
+
+  /**
+   * Unregisters the {@code ZipkinExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+   *     unregistered.
+   */
+  @VisibleForTesting
+  static void unregister(SpanExporter spanExporter) {
+    ZipkinTraceExporter.unregister(spanExporter);
+  }
+}
diff --git a/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java
new file mode 100644
index 0000000..70bc725
--- /dev/null
+++ b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java
@@ -0,0 +1,215 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import zipkin2.Endpoint;
+import zipkin2.Span;
+import zipkin2.codec.SpanBytesEncoder;
+import zipkin2.reporter.Sender;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+final class ZipkinExporterHandler extends SpanExporter.Handler {
+  private static final Tracer tracer = Tracing.getTracer();
+  private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+  private static final Logger logger = Logger.getLogger(ZipkinExporterHandler.class.getName());
+
+  private static final String STATUS_CODE = "census.status_code";
+  private static final String STATUS_DESCRIPTION = "census.status_description";
+  private final SpanBytesEncoder encoder;
+  private final Sender sender;
+  private final Endpoint localEndpoint;
+
+  ZipkinExporterHandler(SpanBytesEncoder encoder, Sender sender, String serviceName) {
+    this.encoder = encoder;
+    this.sender = sender;
+    this.localEndpoint = produceLocalEndpoint(serviceName);
+  }
+
+  /** Logic borrowed from brave.internal.Platform.produceLocalEndpoint */
+  static Endpoint produceLocalEndpoint(String serviceName) {
+    Endpoint.Builder builder = Endpoint.newBuilder().serviceName(serviceName);
+    try {
+      Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces();
+      if (nics == null) {
+        return builder.build();
+      }
+      while (nics.hasMoreElements()) {
+        NetworkInterface nic = nics.nextElement();
+        Enumeration<InetAddress> addresses = nic.getInetAddresses();
+        while (addresses.hasMoreElements()) {
+          InetAddress address = addresses.nextElement();
+          if (address.isSiteLocalAddress()) {
+            builder.ip(address);
+            break;
+          }
+        }
+      }
+    } catch (Exception e) {
+      // don't crash the caller if there was a problem reading nics.
+      if (logger.isLoggable(Level.FINE)) {
+        logger.log(Level.FINE, "error reading nics", e);
+      }
+    }
+    return builder.build();
+  }
+
+  @SuppressWarnings("deprecation")
+  static Span generateSpan(SpanData spanData, Endpoint localEndpoint) {
+    SpanContext context = spanData.getContext();
+    long startTimestamp = toEpochMicros(spanData.getStartTimestamp());
+
+    // TODO(sebright): Fix the Checker Framework warning.
+    @SuppressWarnings("nullness")
+    long endTimestamp = toEpochMicros(spanData.getEndTimestamp());
+
+    // TODO(bdrutu): Fix the Checker Framework warning.
+    @SuppressWarnings("nullness")
+    Span.Builder spanBuilder =
+        Span.newBuilder()
+            .traceId(context.getTraceId().toLowerBase16())
+            .id(context.getSpanId().toLowerBase16())
+            .kind(toSpanKind(spanData))
+            .name(spanData.getName())
+            .timestamp(toEpochMicros(spanData.getStartTimestamp()))
+            .duration(endTimestamp - startTimestamp)
+            .localEndpoint(localEndpoint);
+
+    if (spanData.getParentSpanId() != null && spanData.getParentSpanId().isValid()) {
+      spanBuilder.parentId(spanData.getParentSpanId().toLowerBase16());
+    }
+
+    for (Map.Entry<String, AttributeValue> label :
+        spanData.getAttributes().getAttributeMap().entrySet()) {
+      spanBuilder.putTag(label.getKey(), attributeValueToString(label.getValue()));
+    }
+    Status status = spanData.getStatus();
+    if (status != null) {
+      spanBuilder.putTag(STATUS_CODE, status.getCanonicalCode().toString());
+      if (status.getDescription() != null) {
+        spanBuilder.putTag(STATUS_DESCRIPTION, status.getDescription());
+      }
+    }
+
+    for (TimedEvent<Annotation> annotation : spanData.getAnnotations().getEvents()) {
+      spanBuilder.addAnnotation(
+          toEpochMicros(annotation.getTimestamp()), annotation.getEvent().getDescription());
+    }
+
+    for (TimedEvent<io.opencensus.trace.MessageEvent> messageEvent :
+        spanData.getMessageEvents().getEvents()) {
+      spanBuilder.addAnnotation(
+          toEpochMicros(messageEvent.getTimestamp()), messageEvent.getEvent().getType().name());
+    }
+
+    return spanBuilder.build();
+  }
+
+  @javax.annotation.Nullable
+  private static Span.Kind toSpanKind(SpanData spanData) {
+    // This is a hack because the Span API did not have SpanKind.
+    if (spanData.getKind() == Kind.SERVER
+        || (spanData.getKind() == null && Boolean.TRUE.equals(spanData.getHasRemoteParent()))) {
+      return Span.Kind.SERVER;
+    }
+
+    // This is a hack because the Span API did not have SpanKind.
+    if (spanData.getKind() == Kind.CLIENT || spanData.getName().startsWith("Sent.")) {
+      return Span.Kind.CLIENT;
+    }
+
+    return null;
+  }
+
+  private static long toEpochMicros(Timestamp timestamp) {
+    return SECONDS.toMicros(timestamp.getSeconds()) + NANOSECONDS.toMicros(timestamp.getNanos());
+  }
+
+  // The return type needs to be nullable when this function is used as an argument to 'match' in
+  // attributeValueToString, because 'match' doesn't allow covariant return types.
+  private static final Function<Object, /*@Nullable*/ String> returnToString =
+      Functions.returnToString();
+
+  // TODO: Fix the Checker Framework warning.
+  @SuppressWarnings("nullness")
+  private static String attributeValueToString(AttributeValue attributeValue) {
+    return attributeValue.match(
+        returnToString,
+        returnToString,
+        returnToString,
+        returnToString,
+        Functions.<String>returnConstant(""));
+  }
+
+  @Override
+  public void export(Collection<SpanData> spanDataList) {
+    // Start a new span with explicit 1/10000 sampling probability to avoid the case when user
+    // sets the default sampler to always sample and we get the gRPC span of the zipkin
+    // export call always sampled and go to an infinite loop.
+    Scope scope =
+        tracer.spanBuilder("SendZipkinSpans").setSampler(probabilitySampler).startScopedSpan();
+    try {
+      List<byte[]> encodedSpans = new ArrayList<byte[]>(spanDataList.size());
+      for (SpanData spanData : spanDataList) {
+        encodedSpans.add(encoder.encode(generateSpan(spanData, localEndpoint)));
+      }
+      try {
+        sender.sendSpans(encodedSpans).execute();
+      } catch (IOException e) {
+        tracer
+            .getCurrentSpan()
+            .setStatus(
+                Status.UNKNOWN.withDescription(
+                    e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
+        throw new RuntimeException(e); // TODO: should we instead do drop metrics?
+      }
+    } finally {
+      scope.close();
+    }
+  }
+}
diff --git a/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java
new file mode 100644
index 0000000..aad5a56
--- /dev/null
+++ b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java
@@ -0,0 +1,124 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import zipkin2.Span;
+import zipkin2.codec.SpanBytesEncoder;
+import zipkin2.reporter.Sender;
+import zipkin2.reporter.urlconnection.URLConnectionSender;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Zipkin.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ *   ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "myservicename");
+ *   ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+public final class ZipkinTraceExporter {
+
+  private static final String REGISTER_NAME = ZipkinTraceExporter.class.getName();
+  private static final Object monitor = new Object();
+
+  @GuardedBy("monitor")
+  @Nullable
+  private static Handler handler = null;
+
+  private ZipkinTraceExporter() {}
+
+  /**
+   * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+   * exporter can be registered at any point.
+   *
+   * @param v2Url Ex http://127.0.0.1:9411/api/v2/spans
+   * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+   * @throws IllegalStateException if a Zipkin exporter is already registered.
+   * @since 0.12
+   */
+  public static void createAndRegister(String v2Url, String serviceName) {
+    createAndRegister(SpanBytesEncoder.JSON_V2, URLConnectionSender.create(v2Url), serviceName);
+  }
+
+  /**
+   * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+   * exporter can be registered at any point.
+   *
+   * @param encoder Usually {@link SpanBytesEncoder#JSON_V2}
+   * @param sender Often, but not necessarily an http sender. This could be Kafka or SQS.
+   * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+   * @throws IllegalStateException if a Zipkin exporter is already registered.
+   * @since 0.12
+   */
+  public static void createAndRegister(
+      SpanBytesEncoder encoder, Sender sender, String serviceName) {
+    synchronized (monitor) {
+      checkState(handler == null, "Zipkin exporter is already registered.");
+      Handler newHandler = new ZipkinExporterHandler(encoder, sender, serviceName);
+      handler = newHandler;
+      register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+    }
+  }
+
+  /**
+   * Registers the {@code ZipkinTraceExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+   */
+  @VisibleForTesting
+  static void register(SpanExporter spanExporter, Handler handler) {
+    spanExporter.registerHandler(REGISTER_NAME, handler);
+  }
+
+  /**
+   * Unregisters the Zipkin Trace exporter from the OpenCensus library.
+   *
+   * @throws IllegalStateException if a Zipkin exporter is not registered.
+   * @since 0.12
+   */
+  public static void unregister() {
+    synchronized (monitor) {
+      checkState(handler != null, "Zipkin exporter is not registered.");
+      unregister(Tracing.getExportComponent().getSpanExporter());
+      handler = null;
+    }
+  }
+
+  /**
+   * Unregisters the {@code ZipkinTraceExporter}.
+   *
+   * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+   *     unregistered.
+   */
+  @VisibleForTesting
+  static void unregister(SpanExporter spanExporter) {
+    spanExporter.unregisterHandler(REGISTER_NAME);
+  }
+}
diff --git a/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java
new file mode 100644
index 0000000..7e29300
--- /dev/null
+++ b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.Attributes;
+import io.opencensus.trace.export.SpanData.Links;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import zipkin2.Endpoint;
+import zipkin2.Span;
+
+/** Unit tests for {@link ZipkinExporterHandler}. */
+@RunWith(JUnit4.class)
+public class ZipkinExporterHandlerTest {
+  private static final Endpoint localEndpoint =
+      Endpoint.newBuilder().serviceName("tweetiebird").build();
+  private static final String TRACE_ID = "d239036e7d5cec116b562147388b35bf";
+  private static final String SPAN_ID = "9cc1e3049173be09";
+  private static final String PARENT_SPAN_ID = "8b03ab423da481c5";
+  private static final Map<String, AttributeValue> attributes = Collections.emptyMap();
+  private static final List<TimedEvent<Annotation>> annotations = Collections.emptyList();
+  private static final List<TimedEvent<MessageEvent>> messageEvents =
+      ImmutableList.of(
+          TimedEvent.create(
+              Timestamp.create(1505855799, 433901068),
+              MessageEvent.builder(Type.RECEIVED, 0).setCompressedMessageSize(7).build()),
+          TimedEvent.create(
+              Timestamp.create(1505855799, 459486280),
+              MessageEvent.builder(Type.SENT, 0).setCompressedMessageSize(13).build()));
+
+  @Test
+  public void generateSpan_NoKindAndRemoteParent() {
+    SpanData data =
+        SpanData.create(
+            SpanContext.create(
+                TraceId.fromLowerBase16(TRACE_ID),
+                SpanId.fromLowerBase16(SPAN_ID),
+                TraceOptions.builder().setIsSampled(true).build()),
+            // TODO SpanId.fromLowerBase16
+            SpanId.fromLowerBase16(PARENT_SPAN_ID),
+            true, /* hasRemoteParent */
+            "Recv.helloworld.Greeter.SayHello", /* name */
+            null, /* kind */
+            Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+            Attributes.create(attributes, 0 /* droppedAttributesCount */),
+            TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+            TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+            Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+            null, /* childSpanCount */
+            Status.OK,
+            Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+    assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+        .isEqualTo(
+            Span.newBuilder()
+                .traceId(TRACE_ID)
+                .parentId(PARENT_SPAN_ID)
+                .id(SPAN_ID)
+                .kind(Span.Kind.SERVER)
+                .name(data.getName())
+                .timestamp(1505855794000000L + 194009601L / 1000)
+                .duration(
+                    (1505855799000000L + 465726528L / 1000)
+                        - (1505855794000000L + 194009601L / 1000))
+                .localEndpoint(localEndpoint)
+                .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+                .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+                .putTag("census.status_code", "OK")
+                .build());
+  }
+
+  @Test
+  public void generateSpan_ServerKind() {
+    SpanData data =
+        SpanData.create(
+            SpanContext.create(
+                TraceId.fromLowerBase16(TRACE_ID),
+                SpanId.fromLowerBase16(SPAN_ID),
+                TraceOptions.builder().setIsSampled(true).build()),
+            // TODO SpanId.fromLowerBase16
+            SpanId.fromLowerBase16(PARENT_SPAN_ID),
+            true, /* hasRemoteParent */
+            "Recv.helloworld.Greeter.SayHello", /* name */
+            Kind.SERVER, /* kind */
+            Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+            Attributes.create(attributes, 0 /* droppedAttributesCount */),
+            TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+            TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+            Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+            null, /* childSpanCount */
+            Status.OK,
+            Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+    assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+        .isEqualTo(
+            Span.newBuilder()
+                .traceId(TRACE_ID)
+                .parentId(PARENT_SPAN_ID)
+                .id(SPAN_ID)
+                .kind(Span.Kind.SERVER)
+                .name(data.getName())
+                .timestamp(1505855794000000L + 194009601L / 1000)
+                .duration(
+                    (1505855799000000L + 465726528L / 1000)
+                        - (1505855794000000L + 194009601L / 1000))
+                .localEndpoint(localEndpoint)
+                .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+                .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+                .putTag("census.status_code", "OK")
+                .build());
+  }
+
+  @Test
+  public void generateSpan_ClientKind() {
+    SpanData data =
+        SpanData.create(
+            SpanContext.create(
+                TraceId.fromLowerBase16(TRACE_ID),
+                SpanId.fromLowerBase16(SPAN_ID),
+                TraceOptions.builder().setIsSampled(true).build()),
+            // TODO SpanId.fromLowerBase16
+            SpanId.fromLowerBase16(PARENT_SPAN_ID),
+            true, /* hasRemoteParent */
+            "Sent.helloworld.Greeter.SayHello", /* name */
+            Kind.CLIENT, /* kind */
+            Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+            Attributes.create(attributes, 0 /* droppedAttributesCount */),
+            TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+            TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+            Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+            null, /* childSpanCount */
+            Status.OK,
+            Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+    assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+        .isEqualTo(
+            Span.newBuilder()
+                .traceId(TRACE_ID)
+                .parentId(PARENT_SPAN_ID)
+                .id(SPAN_ID)
+                .kind(Span.Kind.CLIENT)
+                .name(data.getName())
+                .timestamp(1505855794000000L + 194009601L / 1000)
+                .duration(
+                    (1505855799000000L + 465726528L / 1000)
+                        - (1505855794000000L + 194009601L / 1000))
+                .localEndpoint(localEndpoint)
+                .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+                .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+                .putTag("census.status_code", "OK")
+                .build());
+  }
+
+  @Test
+  public void generateSpan_WithAttributes() {
+    Map<String, AttributeValue> attributeMap = new HashMap<String, AttributeValue>();
+    attributeMap.put("string", AttributeValue.stringAttributeValue("string value"));
+    attributeMap.put("boolean", AttributeValue.booleanAttributeValue(false));
+    attributeMap.put("long", AttributeValue.longAttributeValue(9999L));
+    SpanData data =
+        SpanData.create(
+            SpanContext.create(
+                TraceId.fromLowerBase16(TRACE_ID),
+                SpanId.fromLowerBase16(SPAN_ID),
+                TraceOptions.builder().setIsSampled(true).build()),
+            // TODO SpanId.fromLowerBase16
+            SpanId.fromLowerBase16(PARENT_SPAN_ID),
+            true, /* hasRemoteParent */
+            "Sent.helloworld.Greeter.SayHello", /* name */
+            Kind.CLIENT, /* kind */
+            Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+            Attributes.create(attributeMap, 0 /* droppedAttributesCount */),
+            TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+            TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+            Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+            null, /* childSpanCount */
+            Status.OK,
+            Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+    assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+        .isEqualTo(
+            Span.newBuilder()
+                .traceId(TRACE_ID)
+                .parentId(PARENT_SPAN_ID)
+                .id(SPAN_ID)
+                .kind(Span.Kind.CLIENT)
+                .name(data.getName())
+                .timestamp(1505855794000000L + 194009601L / 1000)
+                .duration(
+                    (1505855799000000L + 465726528L / 1000)
+                        - (1505855794000000L + 194009601L / 1000))
+                .localEndpoint(localEndpoint)
+                .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+                .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+                .putTag("census.status_code", "OK")
+                .putTag("string", "string value")
+                .putTag("boolean", "false")
+                .putTag("long", "9999")
+                .build());
+  }
+}
diff --git a/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java
new file mode 100644
index 0000000..2a032d0
--- /dev/null
+++ b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link ZipkinTraceExporter}. */
+@RunWith(JUnit4.class)
+public class ZipkinTraceExporterTest {
+  @Mock private SpanExporter spanExporter;
+  @Mock private Handler handler;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void registerUnregisterZipkinExporter() {
+    ZipkinTraceExporter.register(spanExporter, handler);
+    verify(spanExporter)
+        .registerHandler(
+            eq("io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter"), same(handler));
+    ZipkinTraceExporter.unregister(spanExporter);
+    verify(spanExporter)
+        .unregisterHandler(eq("io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter"));
+  }
+}
diff --git a/findbugs-exclude.xml b/findbugs-exclude.xml
new file mode 100644
index 0000000..014f9a9
--- /dev/null
+++ b/findbugs-exclude.xml
@@ -0,0 +1,106 @@
+<FindBugsFilter>
+  <Match>
+    <!-- Reason: Null has a different meaning than a zero-length array in this case. -->
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+    <Class name="io.opencensus.stats.MutableDistribution"/>
+    <Method name="getInternalBucketCountsArray"/>
+  </Match>
+  <Match>
+    <!-- Reason: Equal is implemented in the AutoValue generated class. -->
+    <Bug pattern="EQ_COMPARETO_USE_OBJECT_EQUALS"/>
+    <Class name="io.opencensus.common.Timestamp"/>
+    <Method name="compareTo"/>
+  </Match>
+  <Match>
+    <!-- Reason: Equal is implemented in the AutoValue generated class. -->
+    <Bug pattern="EQ_COMPARETO_USE_OBJECT_EQUALS"/>
+    <Class name="io.opencensus.common.Duration"/>
+    <Method name="compareTo"/>
+  </Match>
+  <Match>
+    <!-- Reason: BaseMessageEvent only has two visible subclasses. -->
+    <Bug pattern="BC_UNCONFIRMED_CAST"/>
+    <Class name="io.opencensus.trace.internal.BaseMessageEventUtils"/>
+  </Match>
+  <Match>
+    <!-- Reason: This test is testing for a NPE. -->
+    <Bug pattern="NP_NONNULL_PARAM_VIOLATION"/>
+    <Class name="io.opencensus.internal.UtilsTest"/>
+    <Method name="checkNotNull"/>
+  </Match>
+  <Match>
+    <!-- Reason: This test is testing for a NPE. -->
+    <Bug pattern="NP_NONNULL_PARAM_VIOLATION"/>
+    <Class name="io.opencensus.internal.UtilsTest"/>
+    <Method name="checkNotNull_NullErrorMessage"/>
+  </Match>
+  <Match>
+    <!-- Reason: It seems like FindBugs incorrectly assumes that all -->
+    <!-- Throwables are subclasses of Error or Exception. -->
+    <Bug pattern="BC_VACUOUS_INSTANCEOF"/>
+    <Class name="io.opencensus.trace.CurrentSpanUtils$CallableInSpan"/>
+    <Method name="call"/>
+  </Match>
+  <Match>
+    <!-- Reason: Protobuf auto-generated code. -->
+    <Bug pattern="UCF_USELESS_CONTROL_FLOW"/>
+    <Class name="io.opencensus.contrib.appengine.standard.util.TraceIdProto$Builder"/>
+    <Method name="maybeForceBuilderInitialization"/>
+  </Match>
+  <Match>
+    <!-- Reason: The synchronization in the setState is for the side effects not for the state. -->
+    <Bug pattern="UG_SYNC_SET_UNSYNC_GET"/>
+    <Class name="io.opencensus.implcore.stats.StatsComponentImplBase"/>
+  </Match>
+
+  <!-- Suppress some FindBugs warnings related to performance or robustness -->
+  <!-- in test classes, where those issues are less important. -->
+  <Match>
+    <!-- Reason: Only needed for performance. -->
+    <Bug pattern="SIC_INNER_SHOULD_BE_STATIC_ANON"/>
+    <Source name="~.*Test\.java"/>
+  </Match>
+  <Match>
+    <!-- Reason: Only needed for performance. -->
+    <Bug pattern="WMI_WRONG_MAP_ITERATOR"/>
+    <Source name="~.*Test\.java"/>
+  </Match>
+  <Match>
+    <!-- Reason: Only needed for performance. -->
+    <Bug pattern="UM_UNNECESSARY_MATH"/>
+    <Source name="~.*Test\.java"/>
+  </Match>
+  <Match>
+    <!-- Reason: This is less important in a test environment. -->
+    <Bug pattern="DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED"/>
+    <Source name="~.*Test\.java"/>
+  </Match>
+  <Match>
+    <!-- Reason: Many classes initialize fields in @Before methods. -->
+    <Bug pattern="UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"/>
+    <Source name="~.*Test\.java"/>
+  </Match>
+
+  <!-- Suppress all FindBugs warnings about NullPointerExceptions in -->
+  <!-- non-test code. They are redundant with the Checker Framework's -->
+  <!-- warnings, and they sometimes conflict. These warnings are still -->
+  <!-- useful in test code, where we don't use the Checker Framework. -->
+  <Match>
+    <Bug code="NP"/>
+    <Not>
+      <Source name="~.*Test\.java"/>
+    </Not>
+  </Match>
+  <Match>
+    <Bug pattern="UR_UNINIT_READ"/>
+    <Not>
+      <Source name="~.*Test\.java"/>
+    </Not>
+  </Match>
+  <Match>
+    <Bug pattern="UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"/>
+    <Not>
+      <Source name="~.*Test\.java"/>
+    </Not>
+  </Match>
+</FindBugsFilter>
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..758de96
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a95009c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windows variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/impl/README.md b/impl/README.md
new file mode 100644
index 0000000..3dee26f
--- /dev/null
+++ b/impl/README.md
@@ -0,0 +1,5 @@
+OpenCensus Java implementation
+======================================================
+
+* Java 7 compatible.
+* Contains any classes not compatible with Android.
diff --git a/impl/build.gradle b/impl/build.gradle
new file mode 100644
index 0000000..6dacddd
--- /dev/null
+++ b/impl/build.gradle
@@ -0,0 +1,21 @@
+description = 'OpenCensus Implementation'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+    it.sourceCompatibility = 1.7
+    it.targetCompatibility = 1.7
+}
+
+dependencies {
+    compile project(':opencensus-api'),
+            project(':opencensus-impl-core'),
+            libraries.disruptor
+
+    testCompile project(':opencensus-api'),
+            project(':opencensus-impl-core')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
+
+javadoc.exclude 'io/opencensus/internal/**'
\ No newline at end of file
diff --git a/impl/src/main/java/io/opencensus/impl/internal/DisruptorEventQueue.java b/impl/src/main/java/io/opencensus/impl/internal/DisruptorEventQueue.java
new file mode 100644
index 0000000..a0445b5
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/internal/DisruptorEventQueue.java
@@ -0,0 +1,241 @@
+/*
+ * 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.impl.internal;
+
+import com.lmax.disruptor.EventFactory;
+import com.lmax.disruptor.EventHandler;
+import com.lmax.disruptor.RingBuffer;
+import com.lmax.disruptor.SleepingWaitStrategy;
+import com.lmax.disruptor.dsl.Disruptor;
+import com.lmax.disruptor.dsl.ProducerType;
+import io.opencensus.implcore.internal.DaemonThreadFactory;
+import io.opencensus.implcore.internal.EventQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A low-latency event queue for background updating of (possibly contended) objects. This is
+ * intended for use by instrumentation methods to ensure that they do not block foreground
+ * activities. To customize the action taken on reading the queue, derive a new class from {@link
+ * EventQueue.Entry} and pass it to the {@link #enqueue(Entry)} method. The {@link Entry#process()}
+ * method of your class will be called and executed in a background thread. This class is a
+ * Singleton.
+ *
+ * <p>Example Usage: Given a class as follows:
+ *
+ * <pre>
+ * public class someClass {
+ *   public void doSomething() {
+ *     // Do the work of the method. One result is a measurement of something.
+ *     int measurement = doSomeWork();
+ *     // Make an update to the class state, based on this measurement. This work can take some
+ *     // time, but can be done asynchronously, in the background.
+ *     update(measurement);
+ *   }
+ *
+ *   public void update(int arg) {
+ *     // do something
+ *   }
+ * }
+ * </pre>
+ *
+ * <p>The work of calling {@code someClass.update()} can be executed in the backgound as follows:
+ *
+ * <pre>
+ * public class someClass {
+ *   // Add a EventQueueEntry class that will process the update call.
+ *   private static final class SomeClassUpdateEvent implements EventQueueEntry {
+ *     private final SomeClass someClassInstance;
+ *     private final int arg;
+ *
+ *     SomeObjectUpdateEvent(SomeObject someClassInstance, int arg) {
+ *       this.someClassInstance = someClassInstance;
+ *       this.arg = arg;
+ *     }
+ *
+ *     &#064;Override
+ *     public void process() {
+ *       someClassInstance.update(arg);
+ *     }
+ *   }
+ *
+ *   public void doSomething() {
+ *     int measurement = doSomeWork();
+ *     // Instead of calling update() directly, create an event to do the processing, and insert
+ *     // it into the EventQueue. It will be processed in a background thread, and doSomething()
+ *     // can return immediately.
+ *     EventQueue.getInstance.enqueue(new SomeClassUpdateEvent(this, measurement));
+ *   }
+ * }
+ * </pre>
+ */
+@ThreadSafe
+public final class DisruptorEventQueue implements EventQueue {
+
+  private static final Logger logger = Logger.getLogger(DisruptorEventQueue.class.getName());
+
+  // Number of events that can be enqueued at any one time. If more than this are enqueued,
+  // then subsequent attempts to enqueue new entries will block.
+  // TODO(aveitch): consider making this a parameter to the constructor, so the queue can be
+  // configured to a size appropriate to the system (smaller/less busy systems will not need as
+  // large a queue.
+  private static final int DISRUPTOR_BUFFER_SIZE = 8192;
+  // The single instance of the class.
+  private static final DisruptorEventQueue eventQueue = create();
+
+  // The event queue is built on this {@link Disruptor}.
+  private final Disruptor<DisruptorEvent> disruptor;
+  // Ring Buffer for the {@link Disruptor} that underlies the queue.
+  private final RingBuffer<DisruptorEvent> ringBuffer;
+
+  private volatile DisruptorEnqueuer enqueuer;
+
+  // Creates a new EventQueue. Private to prevent creation of non-singleton instance.
+  private DisruptorEventQueue(
+      Disruptor<DisruptorEvent> disruptor,
+      RingBuffer<DisruptorEvent> ringBuffer,
+      DisruptorEnqueuer enqueuer) {
+    this.disruptor = disruptor;
+    this.ringBuffer = ringBuffer;
+    this.enqueuer = enqueuer;
+  }
+
+  // Creates a new EventQueue. Private to prevent creation of non-singleton instance.
+  private static DisruptorEventQueue create() {
+    // Create new Disruptor for processing. Note that Disruptor creates a single thread per
+    // consumer (see https://github.com/LMAX-Exchange/disruptor/issues/121 for details);
+    // this ensures that the event handler can take unsynchronized actions whenever possible.
+    Disruptor<DisruptorEvent> disruptor =
+        new Disruptor<>(
+            DisruptorEventFactory.INSTANCE,
+            DISRUPTOR_BUFFER_SIZE,
+            new DaemonThreadFactory("OpenCensus.Disruptor"),
+            ProducerType.MULTI,
+            new SleepingWaitStrategy());
+    disruptor.handleEventsWith(new DisruptorEventHandler[] {DisruptorEventHandler.INSTANCE});
+    disruptor.start();
+    final RingBuffer<DisruptorEvent> ringBuffer = disruptor.getRingBuffer();
+
+    DisruptorEnqueuer enqueuer =
+        new DisruptorEnqueuer() {
+          @Override
+          public void enqueue(Entry entry) {
+            long sequence = ringBuffer.next();
+            try {
+              DisruptorEvent event = ringBuffer.get(sequence);
+              event.setEntry(entry);
+            } finally {
+              ringBuffer.publish(sequence);
+            }
+          }
+        };
+    return new DisruptorEventQueue(disruptor, ringBuffer, enqueuer);
+  }
+
+  /**
+   * Returns the {@link DisruptorEventQueue} instance.
+   *
+   * @return the singleton {@code EventQueue} instance.
+   */
+  public static DisruptorEventQueue getInstance() {
+    return eventQueue;
+  }
+
+  /**
+   * Enqueues an event on the {@link DisruptorEventQueue}.
+   *
+   * @param entry a class encapsulating the actions to be taken for event processing.
+   */
+  @Override
+  public void enqueue(Entry entry) {
+    enqueuer.enqueue(entry);
+  }
+
+  /** Shuts down the underlying disruptor. */
+  @Override
+  public void shutdown() {
+    enqueuer =
+        new DisruptorEnqueuer() {
+          final AtomicBoolean logged = new AtomicBoolean(false);
+
+          @Override
+          public void enqueue(Entry entry) {
+            if (!logged.getAndSet(true)) {
+              logger.log(Level.INFO, "Attempted to enqueue entry after Disruptor shutdown.");
+            }
+          }
+        };
+
+    disruptor.shutdown();
+  }
+
+  // Allows this event queue to safely shutdown by not enqueuing events on the ring buffer
+  private abstract static class DisruptorEnqueuer {
+
+    public abstract void enqueue(Entry entry);
+  }
+
+  // An event in the {@link EventQueue}. Just holds a reference to an EventQueue.Entry.
+  private static final class DisruptorEvent {
+
+    // TODO(bdrutu): Investigate if volatile is needed. This object is shared between threads so
+    // intuitively this variable must be volatile.
+    @Nullable private volatile Entry entry = null;
+
+    // Sets the EventQueueEntry associated with this DisruptorEvent.
+    void setEntry(@Nullable Entry entry) {
+      this.entry = entry;
+    }
+
+    // Returns the EventQueueEntry associated with this DisruptorEvent.
+    @Nullable
+    Entry getEntry() {
+      return entry;
+    }
+  }
+
+  // Factory for DisruptorEvent.
+  private enum DisruptorEventFactory implements EventFactory<DisruptorEvent> {
+    INSTANCE;
+
+    @Override
+    public DisruptorEvent newInstance() {
+      return new DisruptorEvent();
+    }
+  }
+
+  /**
+   * Every event that gets added to {@link EventQueue} will get processed here. Just calls the
+   * underlying process() method.
+   */
+  private enum DisruptorEventHandler implements EventHandler<DisruptorEvent> {
+    INSTANCE;
+
+    @Override
+    public void onEvent(DisruptorEvent event, long sequence, boolean endOfBatch) {
+      Entry entry = event.getEntry();
+      if (entry != null) {
+        entry.process();
+      }
+      // Remove the reference to the previous entry to allow the memory to be gc'ed.
+      event.setEntry(null);
+    }
+  }
+}
diff --git a/impl/src/main/java/io/opencensus/impl/metrics/MetricsComponentImpl.java b/impl/src/main/java/io/opencensus/impl/metrics/MetricsComponentImpl.java
new file mode 100644
index 0000000..53c354f
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/metrics/MetricsComponentImpl.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018, 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.impl.metrics;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.metrics.MetricsComponentImplBase;
+import io.opencensus.metrics.MetricsComponent;
+
+/** Implementation of {@link MetricsComponent}. */
+public final class MetricsComponentImpl extends MetricsComponentImplBase {
+
+  public MetricsComponentImpl() {
+    super(MillisClock.getInstance());
+  }
+}
diff --git a/impl/src/main/java/io/opencensus/impl/stats/StatsComponentImpl.java b/impl/src/main/java/io/opencensus/impl/stats/StatsComponentImpl.java
new file mode 100644
index 0000000..6b9fe69
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/stats/StatsComponentImpl.java
@@ -0,0 +1,31 @@
+/*
+ * 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.impl.stats;
+
+import io.opencensus.impl.internal.DisruptorEventQueue;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.stats.StatsComponentImplBase;
+import io.opencensus.stats.StatsComponent;
+
+/** Java 7 and 8 implementation of {@link StatsComponent}. */
+public final class StatsComponentImpl extends StatsComponentImplBase {
+
+  /** Public constructor to be used with reflection loading. */
+  public StatsComponentImpl() {
+    super(DisruptorEventQueue.getInstance(), MillisClock.getInstance());
+  }
+}
diff --git a/impl/src/main/java/io/opencensus/impl/tags/TagsComponentImpl.java b/impl/src/main/java/io/opencensus/impl/tags/TagsComponentImpl.java
new file mode 100644
index 0000000..8dd1f37
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/tags/TagsComponentImpl.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.impl.tags;
+
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.TagsComponent;
+
+/** Java 7 and 8 implementation of {@link TagsComponent}. */
+public final class TagsComponentImpl extends TagsComponentImplBase {}
diff --git a/impl/src/main/java/io/opencensus/impl/trace/TraceComponentImpl.java b/impl/src/main/java/io/opencensus/impl/trace/TraceComponentImpl.java
new file mode 100644
index 0000000..1cd7002
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/trace/TraceComponentImpl.java
@@ -0,0 +1,67 @@
+/*
+ * 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.impl.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.impl.internal.DisruptorEventQueue;
+import io.opencensus.impl.trace.internal.ThreadLocalRandomHandler;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.trace.TraceComponentImplBase;
+import io.opencensus.trace.TraceComponent;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/** Java 7 and 8 implementation of the {@link TraceComponent}. */
+public final class TraceComponentImpl extends TraceComponent {
+  private final TraceComponentImplBase traceComponentImplBase;
+
+  /** Public constructor to be used with reflection loading. */
+  public TraceComponentImpl() {
+    traceComponentImplBase =
+        new TraceComponentImplBase(
+            MillisClock.getInstance(),
+            new ThreadLocalRandomHandler(),
+            DisruptorEventQueue.getInstance());
+  }
+
+  @Override
+  public Tracer getTracer() {
+    return traceComponentImplBase.getTracer();
+  }
+
+  @Override
+  public PropagationComponent getPropagationComponent() {
+    return traceComponentImplBase.getPropagationComponent();
+  }
+
+  @Override
+  public Clock getClock() {
+    return traceComponentImplBase.getClock();
+  }
+
+  @Override
+  public ExportComponent getExportComponent() {
+    return traceComponentImplBase.getExportComponent();
+  }
+
+  @Override
+  public TraceConfig getTraceConfig() {
+    return traceComponentImplBase.getTraceConfig();
+  }
+}
diff --git a/impl/src/main/java/io/opencensus/impl/trace/internal/ThreadLocalRandomHandler.java b/impl/src/main/java/io/opencensus/impl/trace/internal/ThreadLocalRandomHandler.java
new file mode 100644
index 0000000..d13e398
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/trace/internal/ThreadLocalRandomHandler.java
@@ -0,0 +1,35 @@
+/*
+ * 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.impl.trace.internal;
+
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Implementation of the {@link RandomHandler} using {@link ThreadLocalRandom}. */
+@ThreadSafe
+public final class ThreadLocalRandomHandler extends RandomHandler {
+
+  /** Constructs a new {@code ThreadLocalRandomHandler}. */
+  public ThreadLocalRandomHandler() {}
+
+  @Override
+  public Random current() {
+    return ThreadLocalRandom.current();
+  }
+}
diff --git a/impl/src/main/java/io/opencensus/trace/TraceComponentImpl.java b/impl/src/main/java/io/opencensus/trace/TraceComponentImpl.java
new file mode 100644
index 0000000..76da3bd
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/trace/TraceComponentImpl.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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.impl.internal.DisruptorEventQueue;
+import io.opencensus.impl.trace.internal.ThreadLocalRandomHandler;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.trace.TraceComponentImplBase;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/** Java 7 and 8 implementation of the {@link TraceComponent}. */
+// TraceComponentImpl was moved to io.opencensus.impl.trace. This class exists for backwards
+// compatibility, so that it can be loaded by opencensus-api 0.5.
+@Deprecated
+public final class TraceComponentImpl extends TraceComponent {
+  private final TraceComponentImplBase traceComponentImplBase;
+
+  /** Public constructor to be used with reflection loading. */
+  public TraceComponentImpl() {
+    traceComponentImplBase =
+        new TraceComponentImplBase(
+            MillisClock.getInstance(),
+            new ThreadLocalRandomHandler(),
+            DisruptorEventQueue.getInstance());
+  }
+
+  @Override
+  public Tracer getTracer() {
+    return traceComponentImplBase.getTracer();
+  }
+
+  @Override
+  public PropagationComponent getPropagationComponent() {
+    return traceComponentImplBase.getPropagationComponent();
+  }
+
+  @Override
+  public Clock getClock() {
+    return traceComponentImplBase.getClock();
+  }
+
+  @Override
+  public ExportComponent getExportComponent() {
+    return traceComponentImplBase.getExportComponent();
+  }
+
+  @Override
+  public TraceConfig getTraceConfig() {
+    return traceComponentImplBase.getTraceConfig();
+  }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/internal/DisruptorEventQueueTest.java b/impl/src/test/java/io/opencensus/impl/internal/DisruptorEventQueueTest.java
new file mode 100644
index 0000000..f12498f
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/internal/DisruptorEventQueueTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016-17, 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.impl.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.internal.EventQueue;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DisruptorEventQueue}. */
+@RunWith(JUnit4.class)
+public class DisruptorEventQueueTest {
+  // Simple class to use that keeps an incrementing counter. Will fail with an assertion if
+  // increment is used from multiple threads, or if the stored value is different from that expected
+  // by the caller.
+  private static class Counter {
+    private int count;
+    private volatile long id; // stores thread ID used in first increment operation.
+
+    public Counter() {
+      count = 0;
+      id = -1;
+    }
+
+    // Increments counter by 1. Will fail in assertion if multiple different threads are used
+    // (the EventQueue backend should be single-threaded).
+    public void increment() {
+      long tid = Thread.currentThread().getId();
+      if (id == -1) {
+        assertThat(count).isEqualTo(0);
+        id = tid;
+      } else {
+        assertThat(id).isEqualTo(tid);
+      }
+      count++;
+    }
+
+    // Check the current value of the counter. Assert if it is not the expected value.
+    public void check(int value) {
+      assertThat(count).isEqualTo(value);
+    }
+  }
+
+  // EventQueueEntry for incrementing a Counter.
+  private static class IncrementEvent implements EventQueue.Entry {
+    private final Counter counter;
+
+    IncrementEvent(Counter counter) {
+      this.counter = counter;
+    }
+
+    @Override
+    public void process() {
+      counter.increment();
+    }
+  }
+
+  @Test
+  public void incrementOnce() {
+    Counter counter = new Counter();
+    IncrementEvent ie = new IncrementEvent(counter);
+    DisruptorEventQueue.getInstance().enqueue(ie);
+    // Sleep briefly, to allow background operations to complete.
+    try {
+      Thread.sleep(500);
+    } catch (InterruptedException ex) {
+      Thread.currentThread().interrupt();
+    }
+    counter.check(1);
+  }
+
+  @Test
+  public void incrementTenK() {
+    final int tenK = 10000;
+    Counter counter = new Counter();
+    for (int i = 0; i < tenK; i++) {
+      IncrementEvent ie = new IncrementEvent(counter);
+      DisruptorEventQueue.getInstance().enqueue(ie);
+    }
+    // Sleep briefly, to allow background operations to complete.
+    try {
+      Thread.sleep(500);
+    } catch (InterruptedException ex) {
+      Thread.currentThread().interrupt();
+    }
+    counter.check(tenK);
+  }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/metrics/MetricsTest.java b/impl/src/test/java/io/opencensus/impl/metrics/MetricsTest.java
new file mode 100644
index 0000000..439933d
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/metrics/MetricsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018, 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.impl.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.metrics.MetricRegistryImpl;
+import io.opencensus.implcore.metrics.export.ExportComponentImpl;
+import io.opencensus.metrics.Metrics;
+import io.opencensus.metrics.MetricsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link MetricsComponent} through the {@link Metrics} class. */
+@RunWith(JUnit4.class)
+public class MetricsTest {
+
+  @Test
+  public void getExportComponent() {
+    assertThat(Metrics.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+  }
+
+  @Test
+  public void getMetricRegistry() {
+    assertThat(Metrics.getMetricRegistry()).isInstanceOf(MetricRegistryImpl.class);
+  }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/stats/StatsTest.java b/impl/src/test/java/io/opencensus/impl/stats/StatsTest.java
new file mode 100644
index 0000000..23606a4
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/stats/StatsTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016-17, 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.impl.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.stats.StatsRecorderImpl;
+import io.opencensus.implcore.stats.ViewManagerImpl;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link StatsComponent} through the {@link Stats} class. */
+@RunWith(JUnit4.class)
+public final class StatsTest {
+  @Test
+  public void getStatsRecorder() {
+    assertThat(Stats.getStatsRecorder()).isInstanceOf(StatsRecorderImpl.class);
+  }
+
+  @Test
+  public void getViewManager() {
+    assertThat(Stats.getViewManager()).isInstanceOf(ViewManagerImpl.class);
+  }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/tags/TagsTest.java b/impl/src/test/java/io/opencensus/impl/tags/TagsTest.java
new file mode 100644
index 0000000..e94cf25
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/tags/TagsTest.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.impl.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.tags.TaggerImpl;
+import io.opencensus.implcore.tags.propagation.TagPropagationComponentImpl;
+import io.opencensus.tags.Tags;
+import io.opencensus.tags.TagsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link TagsComponent} through the {@link Tags} class. */
+@RunWith(JUnit4.class)
+public final class TagsTest {
+  @Test
+  public void getTagger() {
+    assertThat(Tags.getTagger()).isInstanceOf(TaggerImpl.class);
+  }
+
+  @Test
+  public void getTagContextSerializer() {
+    assertThat(Tags.getTagPropagationComponent()).isInstanceOf(TagPropagationComponentImpl.class);
+  }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/trace/TracingTest.java b/impl/src/test/java/io/opencensus/impl/trace/TracingTest.java
new file mode 100644
index 0000000..e58ce1c
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/trace/TracingTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.impl.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.trace.TracerImpl;
+import io.opencensus.implcore.trace.export.ExportComponentImpl;
+import io.opencensus.trace.TraceComponent;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.propagation.PropagationComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link TraceComponent} through the {@link Tracing} class. */
+@RunWith(JUnit4.class)
+public class TracingTest {
+  @Test
+  public void implementationOfTracer() {
+    assertThat(Tracing.getTracer()).isInstanceOf(TracerImpl.class);
+  }
+
+  @Test
+  public void implementationOfBinaryPropagationHandler() {
+    assertThat(Tracing.getPropagationComponent()).isInstanceOf(PropagationComponent.class);
+  }
+
+  @Test
+  public void implementationOfClock() {
+    assertThat(Tracing.getClock()).isInstanceOf(MillisClock.class);
+  }
+
+  @Test
+  public void implementationOfTraceExporter() {
+    assertThat(Tracing.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+  }
+}
diff --git a/impl_core/README.md b/impl_core/README.md
new file mode 100644
index 0000000..901177c
--- /dev/null
+++ b/impl_core/README.md
@@ -0,0 +1,5 @@
+OpenCensus implementation
+======================================================
+
+* The main implementation shared between Java and Android.
+* Java 7 and Android compatible.
diff --git a/impl_core/build.gradle b/impl_core/build.gradle
new file mode 100644
index 0000000..21158c3
--- /dev/null
+++ b/impl_core/build.gradle
@@ -0,0 +1,17 @@
+description = 'OpenCensus Core Implementation'
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.guava
+
+    compileOnly libraries.auto_value
+
+    testCompile project(':opencensus-api'),
+            project(':opencensus-testing')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
+
+javadoc.exclude 'io/opencensus/internal/**'
+javadoc.exclude 'io/opencensus/trace/internal/**'
\ No newline at end of file
diff --git a/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/B3FormatImplBenchmark.java b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/B3FormatImplBenchmark.java
new file mode 100644
index 0000000..736c370
--- /dev/null
+++ b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/B3FormatImplBenchmark.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018, 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.implcore.trace.propagation;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+
+/** Benchmarks for {@link io.opencensus.implcore.trace.propagation.B3Format}. */
+@State(Scope.Benchmark)
+public class B3FormatImplBenchmark {
+  @State(Scope.Thread)
+  public static class Data {
+    private TextFormatBenchmarkBase textFormatBase;
+    private SpanContext spanContext;
+    private Map<String, String> spanContextHeaders;
+
+    @Setup
+    public void setup() {
+      textFormatBase = new TextFormatBenchmarkBase(new B3Format());
+      Random random = new Random(1234);
+      spanContext =
+          SpanContext.create(
+              TraceId.generateRandomId(random),
+              SpanId.generateRandomId(random),
+              TraceOptions.builder().setIsSampled(random.nextBoolean()).build(),
+              Tracestate.builder().build());
+      spanContextHeaders = new HashMap<String, String>();
+      textFormatBase.inject(spanContext, spanContextHeaders);
+    }
+  }
+
+  /**
+   * This benchmark attempts to measure performance of {@link TextFormat#inject(SpanContext, Object,
+   * Setter)}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public Map<String, String> inject(Data data) {
+    Map<String, String> carrier = new HashMap<String, String>();
+    data.textFormatBase.inject(data.spanContext, carrier);
+    return carrier;
+  }
+
+  /**
+   * This benchmark attempts to measure performance of {@link TextFormat#extract(Object, Getter)}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public SpanContext extract(Data data) throws SpanContextParseException {
+    return data.textFormatBase.extract(data.spanContextHeaders);
+  }
+
+  /**
+   * This benchmark attempts to measure performance of {@link TextFormat#inject(SpanContext, Object,
+   * Setter)} then {@link TextFormat#extract(Object, Getter)}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public SpanContext injectExtract(Data data) throws SpanContextParseException {
+    Map<String, String> carrier = new HashMap<String, String>();
+    data.textFormatBase.inject(data.spanContext, carrier);
+    return data.textFormatBase.extract(carrier);
+  }
+}
diff --git a/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplBenchmark.java b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplBenchmark.java
new file mode 100644
index 0000000..70e590b
--- /dev/null
+++ b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplBenchmark.java
@@ -0,0 +1,90 @@
+/*
+ * 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.implcore.trace.propagation;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.BinaryFormat;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+
+/** Benchmarks for {@link BinaryFormat}. */
+@State(Scope.Benchmark)
+public class BinaryFormatImplBenchmark {
+  @State(Scope.Thread)
+  public static class Data {
+    private BinaryFormat binaryFormat;
+    private SpanContext spanContext;
+    private byte[] spanContextBinary;
+
+    @Setup
+    public void setup() {
+      binaryFormat = new BinaryFormatImpl();
+      Random random = new Random(1234);
+      spanContext =
+          SpanContext.create(
+              TraceId.generateRandomId(random),
+              SpanId.generateRandomId(random),
+              TraceOptions.builder().setIsSampled(random.nextBoolean()).build(),
+              Tracestate.builder().build());
+      spanContextBinary = binaryFormat.toByteArray(spanContext);
+    }
+  }
+
+  /**
+   * This benchmark attempts to measure performance of {@link
+   * BinaryFormat#toBinaryValue(SpanContext)}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public byte[] toBinarySpanContext(Data data) {
+    return data.binaryFormat.toByteArray(data.spanContext);
+  }
+
+  /**
+   * This benchmark attempts to measure performance of {@link BinaryFormat#fromBinaryValue(byte[])}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public SpanContext fromBinarySpanContext(Data data) throws SpanContextParseException {
+    return data.binaryFormat.fromByteArray(data.spanContextBinary);
+  }
+
+  /**
+   * This benchmark attempts to measure performance of {@link
+   * BinaryFormat#toBinaryValue(SpanContext)} then {@link BinaryFormat#fromBinaryValue(byte[])}.
+   */
+  @Benchmark
+  @BenchmarkMode(Mode.SampleTime)
+  @OutputTimeUnit(TimeUnit.NANOSECONDS)
+  public SpanContext toFromBinarySpanContext(Data data) throws SpanContextParseException {
+    return data.binaryFormat.fromByteArray(data.binaryFormat.toByteArray(data.spanContext));
+  }
+}
diff --git a/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/TextFormatBenchmarkBase.java b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/TextFormatBenchmarkBase.java
new file mode 100644
index 0000000..1463692
--- /dev/null
+++ b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/TextFormatBenchmarkBase.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018, 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.implcore.trace.propagation;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** Generic benchmarks for {@link io.opencensus.trace.propagation.TextFormat}. */
+final class TextFormatBenchmarkBase {
+  private static final Setter<Map<String, String>> setter =
+      new Setter<Map<String, String>>() {
+        @Override
+        public void put(Map<String, String> carrier, String key, String value) {
+          carrier.put(key, value);
+        }
+      };
+
+  private static final Getter<Map<String, String>> getter =
+      new Getter<Map<String, String>>() {
+        @Nullable
+        @Override
+        public String get(Map<String, String> carrier, String key) {
+          return carrier.get(key);
+        }
+      };
+
+  private final TextFormat textFormat;
+
+  TextFormatBenchmarkBase(TextFormat textFormat) {
+    this.textFormat = textFormat;
+  }
+
+  void inject(SpanContext spanContext, Map<String, String> carrier) {
+    textFormat.inject(spanContext, carrier, setter);
+  }
+
+  SpanContext extract(Map<String, String> carrier) throws SpanContextParseException {
+    return textFormat.extract(carrier, getter);
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/common/MillisClock.java b/impl_core/src/main/java/io/opencensus/implcore/common/MillisClock.java
new file mode 100644
index 0000000..9862692
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/common/MillisClock.java
@@ -0,0 +1,48 @@
+/*
+ * 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.implcore.common;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** A {@link Clock} that uses {@link System#currentTimeMillis()} and {@link System#nanoTime()}. */
+@ThreadSafe
+public final class MillisClock extends Clock {
+  private static final MillisClock INSTANCE = new MillisClock();
+
+  private MillisClock() {}
+
+  /**
+   * Returns a {@code MillisClock}.
+   *
+   * @return a {@code MillisClock}.
+   */
+  public static MillisClock getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public Timestamp now() {
+    return Timestamp.fromMillis(System.currentTimeMillis());
+  }
+
+  @Override
+  public long nowNanos() {
+    return System.nanoTime();
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/CheckerFrameworkUtils.java b/impl_core/src/main/java/io/opencensus/implcore/internal/CheckerFrameworkUtils.java
new file mode 100644
index 0000000..f08289c
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/CheckerFrameworkUtils.java
@@ -0,0 +1,33 @@
+/*
+ * 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.implcore.internal;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility methods for suppressing nullness warnings and working around Checker Framework issues.
+ */
+public final class CheckerFrameworkUtils {
+  private CheckerFrameworkUtils() {}
+
+  /** Suppresses warnings about a nullable value. */
+  // TODO(sebright): Try to remove all uses of this method.
+  @SuppressWarnings("nullness")
+  public static <T> T castNonNull(@Nullable T arg) {
+    return arg;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/CurrentState.java b/impl_core/src/main/java/io/opencensus/implcore/internal/CurrentState.java
new file mode 100644
index 0000000..d7b1b11
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/CurrentState.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2018, 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.implcore.internal;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** The current state base implementation for stats and tags. */
+@ThreadSafe
+public final class CurrentState {
+
+  /** Current state for stats or tags. */
+  public enum State {
+    /** State that fully enables stats collection or tag propagation. */
+    ENABLED,
+
+    /** State that disables stats collection or tag propagation. */
+    DISABLED
+  }
+
+  private enum InternalState {
+    // Enabled and not read.
+    ENABLED_NOT_READ(State.ENABLED, false),
+
+    // Enabled and read.
+    ENABLED_READ(State.ENABLED, true),
+
+    // Disable and not read.
+    DISABLED_NOT_READ(State.DISABLED, false),
+
+    // Disable and read.
+    DISABLED_READ(State.DISABLED, true);
+
+    private final State state;
+    private final boolean isRead;
+
+    InternalState(State state, boolean isRead) {
+      this.state = state;
+      this.isRead = isRead;
+    }
+  }
+
+  private final AtomicReference<InternalState> currentInternalState;
+
+  /**
+   * Constructs a new {@code CurrentState}.
+   *
+   * @param defaultState the default initial state.
+   */
+  public CurrentState(State defaultState) {
+    this.currentInternalState =
+        new AtomicReference<InternalState>(
+            defaultState == State.ENABLED
+                ? InternalState.ENABLED_NOT_READ
+                : InternalState.DISABLED_NOT_READ);
+  }
+
+  /**
+   * Returns the current state and updates the status as being read.
+   *
+   * @return the current state and updates the status as being read.
+   */
+  public State get() {
+    InternalState internalState = currentInternalState.get();
+    while (!internalState.isRead) {
+      // Slow path, the state is first time read. Change the state only if no other changes
+      // happened between the moment initialState is read and this moment. This ensures that this
+      // method only changes the isRead part of the internal state.
+      currentInternalState.compareAndSet(
+          internalState,
+          internalState.state == State.ENABLED
+              ? InternalState.ENABLED_READ
+              : InternalState.DISABLED_READ);
+      internalState = currentInternalState.get();
+    }
+    return internalState.state;
+  }
+
+  /**
+   * Returns the current state without updating the status as being read.
+   *
+   * @return the current state without updating the status as being read.
+   */
+  public State getInternal() {
+    return currentInternalState.get().state;
+  }
+
+  /**
+   * Sets current state to the given state. Returns true if the current state is changed, false
+   * otherwise.
+   *
+   * @param state the state to be set.
+   * @return true if the current state is changed, false otherwise.
+   */
+  public boolean set(State state) {
+    while (true) {
+      InternalState internalState = currentInternalState.get();
+      checkState(!internalState.isRead, "State was already read, cannot set state.");
+      if (state == internalState.state) {
+        return false;
+      } else {
+        if (!currentInternalState.compareAndSet(
+            internalState,
+            state == State.ENABLED
+                ? InternalState.ENABLED_NOT_READ
+                : InternalState.DISABLED_NOT_READ)) {
+          // The state was changed between the moment the internalState was read and this point.
+          // Some conditions may be not correct, reset at the beginning and recheck all conditions.
+          continue;
+        }
+        return true;
+      }
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/DaemonThreadFactory.java b/impl_core/src/main/java/io/opencensus/implcore/internal/DaemonThreadFactory.java
new file mode 100644
index 0000000..2baa500
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/DaemonThreadFactory.java
@@ -0,0 +1,53 @@
+/*
+ * 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.implcore.internal;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** A {@link ThreadFactory} implementation that starts all {@link Thread} as daemons. */
+public final class DaemonThreadFactory implements ThreadFactory {
+  // AppEngine runtimes have constraints on threading and socket handling
+  // that need to be accommodated.
+  public static final boolean IS_RESTRICTED_APPENGINE =
+      System.getProperty("com.google.appengine.runtime.environment") != null
+          && "1.7".equals(System.getProperty("java.specification.version"));
+  private static final String DELIMITER = "-";
+  private static final ThreadFactory threadFactory = MoreExecutors.platformThreadFactory();
+  private final AtomicInteger threadIdGen = new AtomicInteger();
+  private final String threadPrefix;
+
+  /**
+   * Constructs a new {@code DaemonThreadFactory}.
+   *
+   * @param threadPrefix used to prefix all thread names. (E.g. "CensusDisruptor").
+   */
+  public DaemonThreadFactory(String threadPrefix) {
+    this.threadPrefix = threadPrefix + DELIMITER;
+  }
+
+  @Override
+  public Thread newThread(Runnable r) {
+    Thread thread = threadFactory.newThread(r);
+    if (!IS_RESTRICTED_APPENGINE) {
+      thread.setName(threadPrefix + threadIdGen.getAndIncrement());
+      thread.setDaemon(true);
+    }
+    return thread;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/EventQueue.java b/impl_core/src/main/java/io/opencensus/implcore/internal/EventQueue.java
new file mode 100644
index 0000000..6eb1149
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/EventQueue.java
@@ -0,0 +1,36 @@
+/*
+ * 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.implcore.internal;
+
+/** A queue that processes events. See {@code DisruptorEventQueue} for an example. */
+public interface EventQueue {
+  void enqueue(Entry entry);
+
+  void shutdown();
+
+  /**
+   * Base interface to be used for all entries in {@link EventQueue}. For example usage, see {@code
+   * DisruptorEventQueue}.
+   */
+  interface Entry {
+    /**
+     * Process the event associated with this entry. This will be called for every event in the
+     * associated {@link EventQueue}.
+     */
+    void process();
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/NoopScope.java b/impl_core/src/main/java/io/opencensus/implcore/internal/NoopScope.java
new file mode 100644
index 0000000..51efe89
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/NoopScope.java
@@ -0,0 +1,38 @@
+/*
+ * 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.implcore.internal;
+
+import io.opencensus.common.Scope;
+
+/** A {@link Scope} that does nothing when it is created or closed. */
+public final class NoopScope implements Scope {
+  private static final Scope INSTANCE = new NoopScope();
+
+  private NoopScope() {}
+
+  /**
+   * Returns a {@code NoopScope}.
+   *
+   * @return a {@code NoopScope}.
+   */
+  public static Scope getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public void close() {}
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/SimpleEventQueue.java b/impl_core/src/main/java/io/opencensus/implcore/internal/SimpleEventQueue.java
new file mode 100644
index 0000000..58c61c8
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/SimpleEventQueue.java
@@ -0,0 +1,32 @@
+/*
+ * 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.implcore.internal;
+
+/**
+ * An {@link EventQueue} that processes events in the current thread. This class can be used for
+ * testing.
+ */
+public class SimpleEventQueue implements EventQueue {
+
+  @Override
+  public void enqueue(Entry entry) {
+    entry.process();
+  }
+
+  @Override
+  public void shutdown() {}
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/TimestampConverter.java b/impl_core/src/main/java/io/opencensus/implcore/internal/TimestampConverter.java
new file mode 100644
index 0000000..c70f586
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/TimestampConverter.java
@@ -0,0 +1,51 @@
+/*
+ * 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.implcore.internal;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * This class provides a mechanism for converting {@link System#nanoTime() nanoTime} values to
+ * {@link Timestamp}.
+ */
+@Immutable
+public final class TimestampConverter {
+  private final Timestamp timestamp;
+  private final long nanoTime;
+
+  // Returns a WallTimeConverter initialized to now.
+  public static TimestampConverter now(Clock clock) {
+    return new TimestampConverter(clock.now(), clock.nowNanos());
+  }
+
+  /**
+   * Converts a {@link System#nanoTime() nanoTime} value to {@link Timestamp}.
+   *
+   * @param nanoTime value to convert.
+   * @return the {@code Timestamp} representation of the {@code time}.
+   */
+  public Timestamp convertNanoTime(long nanoTime) {
+    return timestamp.addNanos(nanoTime - this.nanoTime);
+  }
+
+  private TimestampConverter(Timestamp timestamp, long nanoTime) {
+    this.timestamp = timestamp;
+    this.nanoTime = nanoTime;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/Utils.java b/impl_core/src/main/java/io/opencensus/implcore/internal/Utils.java
new file mode 100644
index 0000000..05a039b
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/Utils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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.implcore.internal;
+
+import java.util.List;
+
+/** General internal utility methods. */
+public final class Utils {
+
+  private Utils() {}
+
+  /**
+   * Throws a {@link NullPointerException} if any of the list elements is null.
+   *
+   * @param list the argument list to check for null.
+   * @param errorMessage the message to use for the exception. Will be converted to a string using
+   *     {@link String#valueOf(Object)}.
+   */
+  public static <T> void checkListElementNotNull(
+      List<T> list, @javax.annotation.Nullable Object errorMessage) {
+    for (T element : list) {
+      if (element == null) {
+        throw new NullPointerException(String.valueOf(errorMessage));
+      }
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/VarInt.java b/impl_core/src/main/java/io/opencensus/implcore/internal/VarInt.java
new file mode 100644
index 0000000..944f62f
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/VarInt.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2016-17, 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.implcore.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+/** Common methods to encode and decode varints and varlongs into ByteBuffers and arrays. */
+// CHECKSTYLE:OFF
+@SuppressWarnings("UngroupedOverloads")
+public class VarInt {
+
+  /** Maximum encoded size of 32-bit positive integers (in bytes) */
+  public static final int MAX_VARINT_SIZE = 5;
+
+  /** maximum encoded size of 64-bit longs, and negative 32-bit ints (in bytes) */
+  public static final int MAX_VARLONG_SIZE = 10;
+
+  private VarInt() {}
+
+  /**
+   * Returns the encoding size in bytes of its input value.
+   *
+   * @param i the integer to be measured
+   * @return the encoding size in bytes of its input value
+   */
+  public static int varIntSize(int i) {
+    int result = 0;
+    do {
+      result++;
+      i >>>= 7;
+    } while (i != 0);
+    return result;
+  }
+
+  /**
+   * Reads a varint from src, places its values into the first element of dst and returns the offset
+   * in to src of the first byte after the varint.
+   *
+   * @param src source buffer to retrieve from
+   * @param offset offset within src
+   * @param dst the resulting int value
+   * @return the updated offset after reading the varint
+   */
+  public static int getVarInt(byte[] src, int offset, int[] dst) {
+    int result = 0;
+    int shift = 0;
+    int b;
+    do {
+      if (shift >= 32) {
+        // Out of range
+        throw new IndexOutOfBoundsException("varint too long");
+      }
+      // Get 7 bits from next byte
+      b = src[offset++];
+      result |= (b & 0x7F) << shift;
+      shift += 7;
+    } while ((b & 0x80) != 0);
+    dst[0] = result;
+    return offset;
+  }
+
+  /**
+   * Encodes an integer in a variable-length encoding, 7 bits per byte, into a destination byte[],
+   * following the protocol buffer convention.
+   *
+   * @param v the int value to write to sink
+   * @param sink the sink buffer to write to
+   * @param offset the offset within sink to begin writing
+   * @return the updated offset after writing the varint
+   */
+  public static int putVarInt(int v, byte[] sink, int offset) {
+    do {
+      // Encode next 7 bits + terminator bit
+      int bits = v & 0x7F;
+      v >>>= 7;
+      byte b = (byte) (bits + ((v != 0) ? 0x80 : 0));
+      sink[offset++] = b;
+    } while (v != 0);
+    return offset;
+  }
+
+  /**
+   * Reads a varint from the current position of the given ByteBuffer and returns the decoded value
+   * as 32 bit integer.
+   *
+   * <p>The position of the buffer is advanced to the first byte after the decoded varint.
+   *
+   * @param src the ByteBuffer to get the var int from
+   * @return The integer value of the decoded varint
+   */
+  public static int getVarInt(ByteBuffer src) {
+    int tmp;
+    if ((tmp = src.get()) >= 0) {
+      return tmp;
+    }
+    int result = tmp & 0x7f;
+    if ((tmp = src.get()) >= 0) {
+      result |= tmp << 7;
+    } else {
+      result |= (tmp & 0x7f) << 7;
+      if ((tmp = src.get()) >= 0) {
+        result |= tmp << 14;
+      } else {
+        result |= (tmp & 0x7f) << 14;
+        if ((tmp = src.get()) >= 0) {
+          result |= tmp << 21;
+        } else {
+          result |= (tmp & 0x7f) << 21;
+          result |= (tmp = src.get()) << 28;
+          while (tmp < 0) {
+            // We get into this loop only in the case of overflow.
+            // By doing this, we can call getVarInt() instead of
+            // getVarLong() when we only need an int.
+            tmp = src.get();
+          }
+        }
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Encodes an integer in a variable-length encoding, 7 bits per byte, to a ByteBuffer sink.
+   *
+   * @param v the value to encode
+   * @param sink the ByteBuffer to add the encoded value
+   */
+  public static void putVarInt(int v, ByteBuffer sink) {
+    while (true) {
+      int bits = v & 0x7f;
+      v >>>= 7;
+      if (v == 0) {
+        sink.put((byte) bits);
+        return;
+      }
+      sink.put((byte) (bits | 0x80));
+    }
+  }
+
+  /**
+   * Reads a varint from the given InputStream and returns the decoded value as an int.
+   *
+   * @param inputStream the InputStream to read from
+   */
+  public static int getVarInt(InputStream inputStream) throws IOException {
+    int result = 0;
+    int shift = 0;
+    int b;
+    do {
+      if (shift >= 32) {
+        // Out of range
+        throw new IndexOutOfBoundsException("varint too long");
+      }
+      // Get 7 bits from next byte
+      b = inputStream.read();
+      result |= (b & 0x7F) << shift;
+      shift += 7;
+    } while ((b & 0x80) != 0);
+    return result;
+  }
+
+  /**
+   * Encodes an integer in a variable-length encoding, 7 bits per byte, and writes it to the given
+   * OutputStream.
+   *
+   * @param v the value to encode
+   * @param outputStream the OutputStream to write to
+   */
+  public static void putVarInt(int v, OutputStream outputStream) throws IOException {
+    byte[] bytes = new byte[varIntSize(v)];
+    putVarInt(v, bytes, 0);
+    outputStream.write(bytes);
+  }
+
+  /**
+   * Returns the encoding size in bytes of its input value.
+   *
+   * @param v the long to be measured
+   * @return the encoding size in bytes of a given long value.
+   */
+  public static int varLongSize(long v) {
+    int result = 0;
+    do {
+      result++;
+      v >>>= 7;
+    } while (v != 0);
+    return result;
+  }
+
+  /**
+   * Reads an up to 64 bit long varint from the current position of the given ByteBuffer and returns
+   * the decoded value as long.
+   *
+   * <p>The position of the buffer is advanced to the first byte after the decoded varint.
+   *
+   * @param src the ByteBuffer to get the var int from
+   * @return The integer value of the decoded long varint
+   */
+  public static long getVarLong(ByteBuffer src) {
+    long tmp;
+    if ((tmp = src.get()) >= 0) {
+      return tmp;
+    }
+    long result = tmp & 0x7f;
+    if ((tmp = src.get()) >= 0) {
+      result |= tmp << 7;
+    } else {
+      result |= (tmp & 0x7f) << 7;
+      if ((tmp = src.get()) >= 0) {
+        result |= tmp << 14;
+      } else {
+        result |= (tmp & 0x7f) << 14;
+        if ((tmp = src.get()) >= 0) {
+          result |= tmp << 21;
+        } else {
+          result |= (tmp & 0x7f) << 21;
+          if ((tmp = src.get()) >= 0) {
+            result |= tmp << 28;
+          } else {
+            result |= (tmp & 0x7f) << 28;
+            if ((tmp = src.get()) >= 0) {
+              result |= tmp << 35;
+            } else {
+              result |= (tmp & 0x7f) << 35;
+              if ((tmp = src.get()) >= 0) {
+                result |= tmp << 42;
+              } else {
+                result |= (tmp & 0x7f) << 42;
+                if ((tmp = src.get()) >= 0) {
+                  result |= tmp << 49;
+                } else {
+                  result |= (tmp & 0x7f) << 49;
+                  if ((tmp = src.get()) >= 0) {
+                    result |= tmp << 56;
+                  } else {
+                    result |= (tmp & 0x7f) << 56;
+                    result |= ((long) src.get()) << 63;
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Encodes a long integer in a variable-length encoding, 7 bits per byte, to a ByteBuffer sink.
+   *
+   * @param v the value to encode
+   * @param sink the ByteBuffer to add the encoded value
+   */
+  public static void putVarLong(long v, ByteBuffer sink) {
+    while (true) {
+      int bits = ((int) v) & 0x7f;
+      v >>>= 7;
+      if (v == 0) {
+        sink.put((byte) bits);
+        return;
+      }
+      sink.put((byte) (bits | 0x80));
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImpl.java
new file mode 100644
index 0000000..b7104c9
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImpl.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.DerivedDoubleGauge;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Implementation of {@link DerivedDoubleGauge}. */
+public final class DerivedDoubleGaugeImpl extends DerivedDoubleGauge implements Meter {
+  private final MetricDescriptor metricDescriptor;
+  private final int labelKeysSize;
+
+  @SuppressWarnings("rawtypes")
+  private volatile Map<List<LabelValue>, PointWithFunction> registeredPoints =
+      Collections.<List<LabelValue>, PointWithFunction>emptyMap();
+
+  DerivedDoubleGaugeImpl(String name, String description, String unit, List<LabelKey> labelKeys) {
+    labelKeysSize = labelKeys.size();
+    this.metricDescriptor =
+        MetricDescriptor.create(name, description, unit, Type.GAUGE_DOUBLE, labelKeys);
+  }
+
+  @Override
+  @SuppressWarnings("rawtypes")
+  public synchronized <T> void createTimeSeries(
+      List<LabelValue> labelValues,
+      /*@Nullable*/ T obj,
+      ToDoubleFunction</*@Nullable*/ T> function) {
+    Utils.checkListElementNotNull(
+        checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+    checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+    checkNotNull(function, "function");
+
+    List<LabelValue> labelValuesCopy =
+        Collections.<LabelValue>unmodifiableList(new ArrayList<LabelValue>(labelValues));
+
+    PointWithFunction existingPoint = registeredPoints.get(labelValuesCopy);
+    if (existingPoint != null) {
+      throw new IllegalArgumentException(
+          "A different time series with the same labels already exists.");
+    }
+
+    PointWithFunction newPoint = new PointWithFunction<T>(labelValuesCopy, obj, function);
+    // Updating the map of time series happens under a lock to avoid multiple add operations
+    // to happen in the same time.
+    Map<List<LabelValue>, PointWithFunction> registeredPointsCopy =
+        new LinkedHashMap<List<LabelValue>, PointWithFunction>(registeredPoints);
+    registeredPointsCopy.put(labelValuesCopy, newPoint);
+    registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+  }
+
+  @Override
+  @SuppressWarnings("rawtypes")
+  public synchronized void removeTimeSeries(List<LabelValue> labelValues) {
+    checkNotNull(labelValues, "labelValues");
+
+    Map<List<LabelValue>, PointWithFunction> registeredPointsCopy =
+        new LinkedHashMap<List<LabelValue>, PointWithFunction>(registeredPoints);
+    if (registeredPointsCopy.remove(labelValues) == null) {
+      // The element not present, no need to update the current map of time series.
+      return;
+    }
+    registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+  }
+
+  @Override
+  @SuppressWarnings("rawtypes")
+  public synchronized void clear() {
+    registeredPoints = Collections.<List<LabelValue>, PointWithFunction>emptyMap();
+  }
+
+  /*@Nullable*/
+  @Override
+  @SuppressWarnings("rawtypes")
+  public Metric getMetric(Clock clock) {
+    Map<List<LabelValue>, PointWithFunction> currentRegisteredPoints = registeredPoints;
+    if (currentRegisteredPoints.isEmpty()) {
+      return null;
+    }
+
+    if (currentRegisteredPoints.size() == 1) {
+      PointWithFunction point = currentRegisteredPoints.values().iterator().next();
+      return Metric.createWithOneTimeSeries(metricDescriptor, point.getTimeSeries(clock));
+    }
+
+    List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>(currentRegisteredPoints.size());
+    for (Map.Entry<List<LabelValue>, PointWithFunction> entry :
+        currentRegisteredPoints.entrySet()) {
+      timeSeriesList.add(entry.getValue().getTimeSeries(clock));
+    }
+    return Metric.create(metricDescriptor, timeSeriesList);
+  }
+
+  /** Implementation of {@link PointWithFunction} with an object and a callback function. */
+  public static final class PointWithFunction<T> {
+    private final List<LabelValue> labelValues;
+    @javax.annotation.Nullable private final WeakReference<T> ref;
+    private final ToDoubleFunction</*@Nullable*/ T> function;
+
+    PointWithFunction(
+        List<LabelValue> labelValues,
+        /*@Nullable*/ T obj,
+        ToDoubleFunction</*@Nullable*/ T> function) {
+      this.labelValues = labelValues;
+      ref = obj != null ? new WeakReference<T>(obj) : null;
+      this.function = function;
+    }
+
+    private TimeSeries getTimeSeries(Clock clock) {
+      final T obj = ref != null ? ref.get() : null;
+      double value = function.applyAsDouble(obj);
+
+      // TODO(mayurkale): OPTIMIZATION: Avoid re-evaluate the labelValues all the time (issue#1490).
+      return TimeSeries.createWithOnePoint(
+          labelValues, Point.create(Value.doubleValue(value), clock.now()), null);
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedLongGaugeImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedLongGaugeImpl.java
new file mode 100644
index 0000000..90e3e70
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedLongGaugeImpl.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.DerivedLongGauge;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Implementation of {@link DerivedLongGauge}. */
+public final class DerivedLongGaugeImpl extends DerivedLongGauge implements Meter {
+  private final MetricDescriptor metricDescriptor;
+  private final int labelKeysSize;
+
+  @SuppressWarnings("rawtypes")
+  private volatile Map<List<LabelValue>, PointWithFunction> registeredPoints =
+      Collections.<List<LabelValue>, PointWithFunction>emptyMap();
+
+  DerivedLongGaugeImpl(String name, String description, String unit, List<LabelKey> labelKeys) {
+    labelKeysSize = labelKeys.size();
+    this.metricDescriptor =
+        MetricDescriptor.create(name, description, unit, Type.GAUGE_INT64, labelKeys);
+  }
+
+  @Override
+  @SuppressWarnings("rawtypes")
+  public synchronized <T> void createTimeSeries(
+      List<LabelValue> labelValues, /*@Nullable*/ T obj, ToLongFunction</*@Nullable*/ T> function) {
+    Utils.checkListElementNotNull(
+        checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+    checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+    checkNotNull(function, "function");
+
+    List<LabelValue> labelValuesCopy =
+        Collections.unmodifiableList(new ArrayList<LabelValue>(labelValues));
+
+    PointWithFunction existingPoint = registeredPoints.get(labelValuesCopy);
+    if (existingPoint != null) {
+      throw new IllegalArgumentException(
+          "A different time series with the same labels already exists.");
+    }
+
+    PointWithFunction newPoint = new PointWithFunction<T>(labelValuesCopy, obj, function);
+    // Updating the map of time series happens under a lock to avoid multiple add operations
+    // to happen in the same time.
+    Map<List<LabelValue>, PointWithFunction> registeredPointsCopy =
+        new LinkedHashMap<List<LabelValue>, PointWithFunction>(registeredPoints);
+    registeredPointsCopy.put(labelValuesCopy, newPoint);
+    registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+  }
+
+  @Override
+  @SuppressWarnings("rawtypes")
+  public synchronized void removeTimeSeries(List<LabelValue> labelValues) {
+    checkNotNull(labelValues, "labelValues");
+
+    Map<List<LabelValue>, PointWithFunction> registeredPointsCopy =
+        new LinkedHashMap<List<LabelValue>, PointWithFunction>(registeredPoints);
+    if (registeredPointsCopy.remove(labelValues) == null) {
+      // The element not present, no need to update the current map of time series.
+      return;
+    }
+    registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+  }
+
+  @Override
+  @SuppressWarnings("rawtypes")
+  public synchronized void clear() {
+    registeredPoints = Collections.<List<LabelValue>, PointWithFunction>emptyMap();
+  }
+
+  /*@Nullable*/
+  @Override
+  @SuppressWarnings("rawtypes")
+  public Metric getMetric(Clock clock) {
+    Map<List<LabelValue>, PointWithFunction> currentRegisteredPoints = registeredPoints;
+    if (currentRegisteredPoints.isEmpty()) {
+      return null;
+    }
+
+    if (currentRegisteredPoints.size() == 1) {
+      PointWithFunction point = currentRegisteredPoints.values().iterator().next();
+      return Metric.createWithOneTimeSeries(metricDescriptor, point.getTimeSeries(clock));
+    }
+
+    List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>(currentRegisteredPoints.size());
+    for (Map.Entry<List<LabelValue>, PointWithFunction> entry :
+        currentRegisteredPoints.entrySet()) {
+      timeSeriesList.add(entry.getValue().getTimeSeries(clock));
+    }
+    return Metric.create(metricDescriptor, timeSeriesList);
+  }
+
+  /** Implementation of {@link PointWithFunction} with an object and a callback function. */
+  public static final class PointWithFunction<T> {
+    private final List<LabelValue> labelValues;
+    @javax.annotation.Nullable private final WeakReference<T> ref;
+    private final ToLongFunction</*@Nullable*/ T> function;
+
+    PointWithFunction(
+        List<LabelValue> labelValues,
+        /*@Nullable*/ T obj,
+        ToLongFunction</*@Nullable*/ T> function) {
+      this.labelValues = labelValues;
+      ref = obj != null ? new WeakReference<T>(obj) : null;
+      this.function = function;
+    }
+
+    private TimeSeries getTimeSeries(Clock clock) {
+      final T obj = ref != null ? ref.get() : null;
+      long value = function.applyAsLong(obj);
+
+      // TODO(mayurkale): OPTIMIZATION: Avoid re-evaluate the labelValues all the time (issue#1490).
+      return TimeSeries.createWithOnePoint(
+          labelValues, Point.create(Value.longValue(value), clock.now()), null);
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/DoubleGaugeImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/DoubleGaugeImpl.java
new file mode 100644
index 0000000..c314e98
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/DoubleGaugeImpl.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+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.util.concurrent.AtomicDouble;
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.DoubleGauge;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** Implementation of {@link DoubleGauge}. */
+public final class DoubleGaugeImpl extends DoubleGauge implements Meter {
+  @VisibleForTesting static final LabelValue UNSET_VALUE = LabelValue.create(null);
+
+  private final MetricDescriptor metricDescriptor;
+  private volatile Map<List<LabelValue>, PointImpl> registeredPoints =
+      Collections.<List<LabelValue>, PointImpl>emptyMap();
+  private final int labelKeysSize;
+  private final List<LabelValue> defaultLabelValues;
+
+  DoubleGaugeImpl(String name, String description, String unit, List<LabelKey> labelKeys) {
+    labelKeysSize = labelKeys.size();
+    this.metricDescriptor =
+        MetricDescriptor.create(name, description, unit, Type.GAUGE_DOUBLE, labelKeys);
+
+    // initialize defaultLabelValues
+    defaultLabelValues = new ArrayList<LabelValue>(labelKeysSize);
+    for (int i = 0; i < labelKeysSize; i++) {
+      defaultLabelValues.add(UNSET_VALUE);
+    }
+  }
+
+  @Override
+  public DoublePoint getOrCreateTimeSeries(List<LabelValue> labelValues) {
+    // lock free point retrieval, if it is present
+    PointImpl existingPoint = registeredPoints.get(labelValues);
+    if (existingPoint != null) {
+      return existingPoint;
+    }
+
+    List<LabelValue> labelValuesCopy =
+        Collections.unmodifiableList(
+            new ArrayList<LabelValue>(checkNotNull(labelValues, "labelValues")));
+    return registerTimeSeries(labelValuesCopy);
+  }
+
+  @Override
+  public DoublePoint getDefaultTimeSeries() {
+    // lock free default point retrieval, if it is present
+    PointImpl existingPoint = registeredPoints.get(defaultLabelValues);
+    if (existingPoint != null) {
+      return existingPoint;
+    }
+    return registerTimeSeries(Collections.unmodifiableList(defaultLabelValues));
+  }
+
+  @Override
+  public synchronized void removeTimeSeries(List<LabelValue> labelValues) {
+    checkNotNull(labelValues, "labelValues");
+
+    Map<List<LabelValue>, PointImpl> registeredPointsCopy =
+        new LinkedHashMap<List<LabelValue>, PointImpl>(registeredPoints);
+    if (registeredPointsCopy.remove(labelValues) == null) {
+      // The element not present, no need to update the current map of points.
+      return;
+    }
+    registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+  }
+
+  @Override
+  public synchronized void clear() {
+    registeredPoints = Collections.<List<LabelValue>, PointImpl>emptyMap();
+  }
+
+  private synchronized DoublePoint registerTimeSeries(List<LabelValue> labelValues) {
+    PointImpl existingPoint = registeredPoints.get(labelValues);
+    if (existingPoint != null) {
+      // Return a Point that are already registered. This can happen if a multiple threads
+      // concurrently try to register the same {@code TimeSeries}.
+      return existingPoint;
+    }
+
+    checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+    Utils.checkListElementNotNull(labelValues, "labelValue element should not be null.");
+
+    PointImpl newPoint = new PointImpl(labelValues);
+    // Updating the map of points happens under a lock to avoid multiple add operations
+    // to happen in the same time.
+    Map<List<LabelValue>, PointImpl> registeredPointsCopy =
+        new LinkedHashMap<List<LabelValue>, PointImpl>(registeredPoints);
+    registeredPointsCopy.put(labelValues, newPoint);
+    registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+
+    return newPoint;
+  }
+
+  @Nullable
+  @Override
+  public Metric getMetric(Clock clock) {
+    Map<List<LabelValue>, PointImpl> currentRegisteredPoints = registeredPoints;
+    if (currentRegisteredPoints.isEmpty()) {
+      return null;
+    }
+
+    if (currentRegisteredPoints.size() == 1) {
+      PointImpl point = currentRegisteredPoints.values().iterator().next();
+      return Metric.createWithOneTimeSeries(metricDescriptor, point.getTimeSeries(clock));
+    }
+
+    List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>(currentRegisteredPoints.size());
+    for (Map.Entry<List<LabelValue>, PointImpl> entry : currentRegisteredPoints.entrySet()) {
+      timeSeriesList.add(entry.getValue().getTimeSeries(clock));
+    }
+    return Metric.create(metricDescriptor, timeSeriesList);
+  }
+
+  /** Implementation of {@link DoubleGauge.DoublePoint}. */
+  public static final class PointImpl extends DoublePoint {
+
+    // TODO(mayurkale): Consider to use DoubleAdder here, once we upgrade to Java8.
+    private final AtomicDouble value = new AtomicDouble(0);
+    private final List<LabelValue> labelValues;
+
+    PointImpl(List<LabelValue> labelValues) {
+      this.labelValues = labelValues;
+    }
+
+    @Override
+    public void add(double amt) {
+      value.addAndGet(amt);
+    }
+
+    @Override
+    public void set(double val) {
+      value.set(val);
+    }
+
+    private TimeSeries getTimeSeries(Clock clock) {
+      return TimeSeries.createWithOnePoint(
+          labelValues, Point.create(Value.doubleValue(value.get()), clock.now()), null);
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/LongGaugeImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/LongGaugeImpl.java
new file mode 100644
index 0000000..3460d7a
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/LongGaugeImpl.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.LongGauge;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.annotation.Nullable;
+
+/** Implementation of {@link LongGauge}. */
+public final class LongGaugeImpl extends LongGauge implements Meter {
+  @VisibleForTesting static final LabelValue UNSET_VALUE = LabelValue.create(null);
+
+  private final MetricDescriptor metricDescriptor;
+  private volatile Map<List<LabelValue>, PointImpl> registeredPoints =
+      Collections.<List<LabelValue>, PointImpl>emptyMap();
+  private final int labelKeysSize;
+  private final List<LabelValue> defaultLabelValues;
+
+  LongGaugeImpl(String name, String description, String unit, List<LabelKey> labelKeys) {
+    labelKeysSize = labelKeys.size();
+    this.metricDescriptor =
+        MetricDescriptor.create(name, description, unit, Type.GAUGE_INT64, labelKeys);
+
+    // initialize defaultLabelValues
+    defaultLabelValues = new ArrayList<LabelValue>(labelKeysSize);
+    for (int i = 0; i < labelKeysSize; i++) {
+      defaultLabelValues.add(UNSET_VALUE);
+    }
+  }
+
+  @Override
+  public LongPoint getOrCreateTimeSeries(List<LabelValue> labelValues) {
+    // lock free point retrieval, if it is present
+    PointImpl existingPoint = registeredPoints.get(labelValues);
+    if (existingPoint != null) {
+      return existingPoint;
+    }
+
+    List<LabelValue> labelValuesCopy =
+        Collections.unmodifiableList(
+            new ArrayList<LabelValue>(checkNotNull(labelValues, "labelValues")));
+    return registerTimeSeries(labelValuesCopy);
+  }
+
+  @Override
+  public LongPoint getDefaultTimeSeries() {
+    // lock free default point retrieval, if it is present
+    PointImpl existingPoint = registeredPoints.get(defaultLabelValues);
+    if (existingPoint != null) {
+      return existingPoint;
+    }
+    return registerTimeSeries(Collections.unmodifiableList(defaultLabelValues));
+  }
+
+  @Override
+  public synchronized void removeTimeSeries(List<LabelValue> labelValues) {
+    checkNotNull(labelValues, "labelValues");
+
+    Map<List<LabelValue>, PointImpl> registeredPointsCopy =
+        new LinkedHashMap<List<LabelValue>, PointImpl>(registeredPoints);
+    if (registeredPointsCopy.remove(labelValues) == null) {
+      // The element not present, no need to update the current map of points.
+      return;
+    }
+    registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+  }
+
+  @Override
+  public synchronized void clear() {
+    registeredPoints = Collections.<List<LabelValue>, PointImpl>emptyMap();
+  }
+
+  private synchronized LongPoint registerTimeSeries(List<LabelValue> labelValues) {
+    PointImpl existingPoint = registeredPoints.get(labelValues);
+    if (existingPoint != null) {
+      // Return a Point that are already registered. This can happen if a multiple threads
+      // concurrently try to register the same {@code TimeSeries}.
+      return existingPoint;
+    }
+
+    checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+    Utils.checkListElementNotNull(labelValues, "labelValue element should not be null.");
+
+    PointImpl newPoint = new PointImpl(labelValues);
+    // Updating the map of points happens under a lock to avoid multiple add operations
+    // to happen in the same time.
+    Map<List<LabelValue>, PointImpl> registeredPointsCopy =
+        new LinkedHashMap<List<LabelValue>, PointImpl>(registeredPoints);
+    registeredPointsCopy.put(labelValues, newPoint);
+    registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+
+    return newPoint;
+  }
+
+  @Nullable
+  @Override
+  public Metric getMetric(Clock clock) {
+    Map<List<LabelValue>, PointImpl> currentRegisteredPoints = registeredPoints;
+    if (currentRegisteredPoints.isEmpty()) {
+      return null;
+    }
+
+    if (currentRegisteredPoints.size() == 1) {
+      PointImpl point = currentRegisteredPoints.values().iterator().next();
+      return Metric.createWithOneTimeSeries(metricDescriptor, point.getTimeSeries(clock));
+    }
+
+    List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>(currentRegisteredPoints.size());
+    for (Map.Entry<List<LabelValue>, PointImpl> entry : currentRegisteredPoints.entrySet()) {
+      timeSeriesList.add(entry.getValue().getTimeSeries(clock));
+    }
+    return Metric.create(metricDescriptor, timeSeriesList);
+  }
+
+  /** Implementation of {@link LongGauge.LongPoint}. */
+  public static final class PointImpl extends LongPoint {
+
+    // TODO(mayurkale): Consider to use LongAdder here, once we upgrade to Java8.
+    private final AtomicLong value = new AtomicLong(0);
+    private final List<LabelValue> labelValues;
+
+    PointImpl(List<LabelValue> labelValues) {
+      this.labelValues = labelValues;
+    }
+
+    @Override
+    public void add(long amt) {
+      value.addAndGet(amt);
+    }
+
+    @Override
+    public void set(long val) {
+      value.set(val);
+    }
+
+    private TimeSeries getTimeSeries(Clock clock) {
+      return TimeSeries.createWithOnePoint(
+          labelValues, Point.create(Value.longValue(value.get()), clock.now()), null);
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/Meter.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/Meter.java
new file mode 100644
index 0000000..f5a8dc8
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/Meter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import io.opencensus.common.Clock;
+import io.opencensus.metrics.export.Metric;
+import javax.annotation.Nullable;
+
+interface Meter {
+  /**
+   * Provides a {@link io.opencensus.metrics.export.Metric} with one or more {@link
+   * io.opencensus.metrics.export.TimeSeries}.
+   *
+   * @param clock the clock used to get the time.
+   * @throws NullPointerException if {@code TimeSeries} is not present in {@code Metric}.
+   * @return a {@code Metric}.
+   */
+  @Nullable
+  Metric getMetric(Clock clock);
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricRegistryImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricRegistryImpl.java
new file mode 100644
index 0000000..1a301ec
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricRegistryImpl.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.DerivedDoubleGauge;
+import io.opencensus.metrics.DerivedLongGauge;
+import io.opencensus.metrics.DoubleGauge;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LongGauge;
+import io.opencensus.metrics.MetricRegistry;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricProducer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Implementation of {@link MetricRegistry}. */
+public final class MetricRegistryImpl extends MetricRegistry {
+  private final RegisteredMeters registeredMeters;
+  private final MetricProducer metricProducer;
+
+  MetricRegistryImpl(Clock clock) {
+    registeredMeters = new RegisteredMeters();
+    metricProducer = new MetricProducerForRegistry(registeredMeters, clock);
+  }
+
+  @Override
+  public LongGauge addLongGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys) {
+    Utils.checkListElementNotNull(
+        checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+    LongGaugeImpl longGaugeMetric =
+        new LongGaugeImpl(
+            checkNotNull(name, "name"),
+            checkNotNull(description, "description"),
+            checkNotNull(unit, "unit"),
+            Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+    registeredMeters.registerMeter(name, longGaugeMetric);
+    return longGaugeMetric;
+  }
+
+  @Override
+  public DoubleGauge addDoubleGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys) {
+    Utils.checkListElementNotNull(
+        checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+    DoubleGaugeImpl doubleGaugeMetric =
+        new DoubleGaugeImpl(
+            checkNotNull(name, "name"),
+            checkNotNull(description, "description"),
+            checkNotNull(unit, "unit"),
+            Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+    registeredMeters.registerMeter(name, doubleGaugeMetric);
+    return doubleGaugeMetric;
+  }
+
+  @Override
+  public DerivedLongGauge addDerivedLongGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys) {
+    Utils.checkListElementNotNull(
+        checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+    DerivedLongGaugeImpl derivedLongGauge =
+        new DerivedLongGaugeImpl(
+            checkNotNull(name, "name"),
+            checkNotNull(description, "description"),
+            checkNotNull(unit, "unit"),
+            Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+    registeredMeters.registerMeter(name, derivedLongGauge);
+    return derivedLongGauge;
+  }
+
+  @Override
+  public DerivedDoubleGauge addDerivedDoubleGauge(
+      String name, String description, String unit, List<LabelKey> labelKeys) {
+    Utils.checkListElementNotNull(
+        checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+    DerivedDoubleGaugeImpl derivedDoubleGauge =
+        new DerivedDoubleGaugeImpl(
+            checkNotNull(name, "name"),
+            checkNotNull(description, "description"),
+            checkNotNull(unit, "unit"),
+            Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+    registeredMeters.registerMeter(name, derivedDoubleGauge);
+    return derivedDoubleGauge;
+  }
+
+  private static final class RegisteredMeters {
+    private volatile Map<String, Meter> registeredMeters = Collections.emptyMap();
+
+    private Map<String, Meter> getRegisteredMeters() {
+      return registeredMeters;
+    }
+
+    private synchronized void registerMeter(String meterName, Meter meter) {
+      Meter existingMeter = registeredMeters.get(meterName);
+      if (existingMeter != null) {
+        // TODO(mayurkale): Allow users to register the same Meter multiple times without exception.
+        throw new IllegalArgumentException(
+            "A different metric with the same name already registered.");
+      }
+
+      Map<String, Meter> registeredMetersCopy = new LinkedHashMap<String, Meter>(registeredMeters);
+      registeredMetersCopy.put(meterName, meter);
+      registeredMeters = Collections.unmodifiableMap(registeredMetersCopy);
+    }
+  }
+
+  private static final class MetricProducerForRegistry extends MetricProducer {
+    private final RegisteredMeters registeredMeters;
+    private final Clock clock;
+
+    private MetricProducerForRegistry(RegisteredMeters registeredMeters, Clock clock) {
+      this.registeredMeters = registeredMeters;
+      this.clock = clock;
+    }
+
+    @Override
+    public Collection<Metric> getMetrics() {
+      // Get a snapshot of the current registered meters.
+      Map<String, Meter> meters = registeredMeters.getRegisteredMeters();
+      if (meters.isEmpty()) {
+        return Collections.emptyList();
+      }
+
+      List<Metric> metrics = new ArrayList<Metric>(meters.size());
+      for (Map.Entry<String, Meter> entry : meters.entrySet()) {
+        Metric metric = entry.getValue().getMetric(clock);
+        if (metric != null) {
+          metrics.add(metric);
+        }
+      }
+      return metrics;
+    }
+  }
+
+  MetricProducer getMetricProducer() {
+    return metricProducer;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricsComponentImplBase.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricsComponentImplBase.java
new file mode 100644
index 0000000..1aef672
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricsComponentImplBase.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.metrics.export.ExportComponentImpl;
+import io.opencensus.metrics.MetricsComponent;
+
+/** Implementation of {@link MetricsComponent}. */
+public class MetricsComponentImplBase extends MetricsComponent {
+
+  private final ExportComponentImpl exportComponent;
+  private final MetricRegistryImpl metricRegistry;
+
+  @Override
+  public ExportComponentImpl getExportComponent() {
+    return exportComponent;
+  }
+
+  @Override
+  public MetricRegistryImpl getMetricRegistry() {
+    return metricRegistry;
+  }
+
+  protected MetricsComponentImplBase(Clock clock) {
+    exportComponent = new ExportComponentImpl();
+    metricRegistry = new MetricRegistryImpl(clock);
+    // Register the MetricRegistry's MetricProducer to the global MetricProducerManager.
+    exportComponent.getMetricProducerManager().add(metricRegistry.getMetricProducer());
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/export/ExportComponentImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/export/ExportComponentImpl.java
new file mode 100644
index 0000000..173c3ae
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/export/ExportComponentImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018, 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.implcore.metrics.export;
+
+import io.opencensus.metrics.export.ExportComponent;
+import io.opencensus.metrics.export.MetricProducerManager;
+
+/** Implementation of {@link ExportComponent}. */
+public final class ExportComponentImpl extends ExportComponent {
+
+  private final MetricProducerManager metricProducerManager = new MetricProducerManagerImpl();
+
+  @Override
+  public MetricProducerManager getMetricProducerManager() {
+    return metricProducerManager;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImpl.java
new file mode 100644
index 0000000..6f585a1
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImpl.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018, 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.implcore.metrics.export;
+
+import com.google.common.base.Preconditions;
+import io.opencensus.metrics.export.MetricProducer;
+import io.opencensus.metrics.export.MetricProducerManager;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Implementation of {@link MetricProducerManager}. */
+@ThreadSafe
+public final class MetricProducerManagerImpl extends MetricProducerManager {
+
+  private volatile Set<MetricProducer> metricProducers =
+      Collections.unmodifiableSet(new LinkedHashSet<MetricProducer>());
+
+  @Override
+  public synchronized void add(MetricProducer metricProducer) {
+    Preconditions.checkNotNull(metricProducer, "metricProducer");
+    // Updating the set of MetricProducers happens under a lock to avoid multiple add or remove
+    // operations to happen in the same time.
+    Set<MetricProducer> newMetricProducers = new LinkedHashSet<MetricProducer>(metricProducers);
+    if (!newMetricProducers.add(metricProducer)) {
+      // The element already present, no need to update the current set of MetricProducers.
+      return;
+    }
+    metricProducers = Collections.unmodifiableSet(newMetricProducers);
+  }
+
+  @Override
+  public synchronized void remove(MetricProducer metricProducer) {
+    Preconditions.checkNotNull(metricProducer, "metricProducer");
+    // Updating the set of MetricProducers happens under a lock to avoid multiple add or remove
+    // operations to happen in the same time.
+    Set<MetricProducer> newMetricProducers = new LinkedHashSet<MetricProducer>(metricProducers);
+    if (!newMetricProducers.remove(metricProducer)) {
+      // The element not present, no need to update the current set of MetricProducers.
+      return;
+    }
+    metricProducers = Collections.unmodifiableSet(newMetricProducers);
+  }
+
+  @Override
+  public Set<MetricProducer> getAllMetricProducer() {
+    return metricProducers;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/IntervalBucket.java b/impl_core/src/main/java/io/opencensus/implcore/stats/IntervalBucket.java
new file mode 100644
index 0000000..172db53
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/IntervalBucket.java
@@ -0,0 +1,95 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.Maps;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Measure;
+import io.opencensus.tags.TagValue;
+import java.util.List;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** The bucket with aggregated {@code MeasureValue}s used for {@code IntervalViewData}. */
+final class IntervalBucket {
+
+  private static final Duration ZERO = Duration.create(0, 0);
+
+  private final Timestamp start;
+  private final Duration duration;
+  private final Aggregation aggregation;
+  private final Measure measure;
+  private final Map<List</*@Nullable*/ TagValue>, MutableAggregation> tagValueAggregationMap =
+      Maps.newHashMap();
+
+  IntervalBucket(Timestamp start, Duration duration, Aggregation aggregation, Measure measure) {
+    this.start = checkNotNull(start, "Start");
+    this.duration = checkNotNull(duration, "Duration");
+    checkArgument(duration.compareTo(ZERO) > 0, "Duration must be positive");
+    this.aggregation = checkNotNull(aggregation, "Aggregation");
+    this.measure = checkNotNull(measure, "measure");
+  }
+
+  Map<List</*@Nullable*/ TagValue>, MutableAggregation> getTagValueAggregationMap() {
+    return tagValueAggregationMap;
+  }
+
+  Timestamp getStart() {
+    return start;
+  }
+
+  // Puts a new value into the internal MutableAggregations, based on the TagValues.
+  void record(
+      List</*@Nullable*/ TagValue> tagValues,
+      double value,
+      Map<String, String> attachments,
+      Timestamp timestamp) {
+    if (!tagValueAggregationMap.containsKey(tagValues)) {
+      tagValueAggregationMap.put(
+          tagValues, RecordUtils.createMutableAggregation(aggregation, measure));
+    }
+    tagValueAggregationMap.get(tagValues).add(value, attachments, timestamp);
+  }
+
+  /*
+   * Returns how much fraction of duration has passed in this IntervalBucket. For example, if this
+   * bucket starts at 10s and has a duration of 20s, and now is 15s, then getFraction() should
+   * return (15 - 10) / 20 = 0.25.
+   *
+   * This IntervalBucket must be current, i.e. the current timestamp must be within
+   * [this.start, this.start + this.duration).
+   */
+  double getFraction(Timestamp now) {
+    Duration elapsedTime = now.subtractTimestamp(start);
+    checkArgument(
+        elapsedTime.compareTo(ZERO) >= 0 && elapsedTime.compareTo(duration) < 0,
+        "This bucket must be current.");
+    return ((double) elapsedTime.toMillis()) / duration.toMillis();
+  }
+
+  void clearStats() {
+    tagValueAggregationMap.clear();
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapImpl.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapImpl.java
new file mode 100644
index 0000000..ee51796
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapImpl.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016-17, 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.implcore.stats;
+
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.unsafe.ContextUtils;
+
+/** Implementation of {@link MeasureMap}. */
+final class MeasureMapImpl extends MeasureMap {
+  private final StatsManager statsManager;
+  private final MeasureMapInternal.Builder builder = MeasureMapInternal.builder();
+
+  static MeasureMapImpl create(StatsManager statsManager) {
+    return new MeasureMapImpl(statsManager);
+  }
+
+  private MeasureMapImpl(StatsManager statsManager) {
+    this.statsManager = statsManager;
+  }
+
+  @Override
+  public MeasureMapImpl put(MeasureDouble measure, double value) {
+    builder.put(measure, value);
+    return this;
+  }
+
+  @Override
+  public MeasureMapImpl put(MeasureLong measure, long value) {
+    builder.put(measure, value);
+    return this;
+  }
+
+  @Override
+  public MeasureMap putAttachment(String key, String value) {
+    builder.putAttachment(key, value);
+    return this;
+  }
+
+  @Override
+  public void record() {
+    // Use the context key directly, to avoid depending on the tags implementation.
+    record(ContextUtils.TAG_CONTEXT_KEY.get());
+  }
+
+  @Override
+  public void record(TagContext tags) {
+    statsManager.record(tags, builder.build());
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapInternal.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapInternal.java
new file mode 100644
index 0000000..d867b34
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapInternal.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2016-17, 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.implcore.stats;
+
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.Measurement;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+// TODO(songya): consider combining MeasureMapImpl and this class.
+/** A map from {@link Measure}'s to measured values. */
+final class MeasureMapInternal {
+
+  /** Returns a {@link Builder} for the {@link MeasureMapInternal} class. */
+  static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Returns an {@link Iterator} over the measure/value mappings in this {@link MeasureMapInternal}.
+   * The {@code Iterator} does not support {@link Iterator#remove()}.
+   */
+  Iterator<Measurement> iterator() {
+    return new MeasureMapInternalIterator();
+  }
+
+  // Returns the contextual information associated with an example value.
+  Map<String, String> getAttachments() {
+    return attachments;
+  }
+
+  private final ArrayList<Measurement> measurements;
+  private final Map<String, String> attachments;
+
+  private MeasureMapInternal(ArrayList<Measurement> measurements, Map<String, String> attachments) {
+    this.measurements = measurements;
+    this.attachments = Collections.unmodifiableMap(new HashMap<String, String>(attachments));
+  }
+
+  /** Builder for the {@link MeasureMapInternal} class. */
+  static class Builder {
+    /**
+     * Associates the {@link MeasureDouble} with the given value. Subsequent updates to the same
+     * {@link MeasureDouble} will overwrite the previous value.
+     *
+     * @param measure the {@link MeasureDouble}
+     * @param value the value to be associated with {@code measure}
+     * @return this
+     */
+    Builder put(MeasureDouble measure, double value) {
+      measurements.add(Measurement.MeasurementDouble.create(measure, value));
+      return this;
+    }
+
+    /**
+     * Associates the {@link MeasureLong} with the given value. Subsequent updates to the same
+     * {@link MeasureLong} will overwrite the previous value.
+     *
+     * @param measure the {@link MeasureLong}
+     * @param value the value to be associated with {@code measure}
+     * @return this
+     */
+    Builder put(MeasureLong measure, long value) {
+      measurements.add(Measurement.MeasurementLong.create(measure, value));
+      return this;
+    }
+
+    Builder putAttachment(String key, String value) {
+      this.attachments.put(key, value);
+      return this;
+    }
+
+    /** Constructs a {@link MeasureMapInternal} from the current measurements. */
+    MeasureMapInternal build() {
+      // Note: this makes adding measurements quadratic but is fastest for the sizes of
+      // MeasureMapInternals that we should see. We may want to go to a strategy of sort/eliminate
+      // for larger MeasureMapInternals.
+      for (int i = measurements.size() - 1; i >= 0; i--) {
+        for (int j = i - 1; j >= 0; j--) {
+          if (measurements.get(i).getMeasure() == measurements.get(j).getMeasure()) {
+            measurements.remove(j);
+            j--;
+          }
+        }
+      }
+      return new MeasureMapInternal(measurements, attachments);
+    }
+
+    private final ArrayList<Measurement> measurements = new ArrayList<Measurement>();
+    private final Map<String, String> attachments = new HashMap<String, String>();
+
+    private Builder() {}
+  }
+
+  // Provides an unmodifiable Iterator over this instance's measurements.
+  private final class MeasureMapInternalIterator implements Iterator<Measurement> {
+    @Override
+    public boolean hasNext() {
+      return position < length;
+    }
+
+    @Override
+    public Measurement next() {
+      if (position >= measurements.size()) {
+        throw new NoSuchElementException();
+      }
+      return measurements.get(position++);
+    }
+
+    @Override
+    public void remove() {
+      throw new UnsupportedOperationException();
+    }
+
+    private final int length = measurements.size();
+    private int position = 0;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureToViewMap.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureToViewMap.java
new file mode 100644
index 0000000..5da0cad
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureToViewMap.java
@@ -0,0 +1,194 @@
+/*
+ * 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.implcore.stats;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measurement;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagContext;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import javax.annotation.concurrent.GuardedBy;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** A class that stores a singleton map from {@code MeasureName}s to {@link MutableViewData}s. */
+@SuppressWarnings("deprecation")
+final class MeasureToViewMap {
+
+  /*
+   * A synchronized singleton map that stores the one-to-many mapping from Measures
+   * to MutableViewDatas.
+   */
+  @GuardedBy("this")
+  private final Multimap<String, MutableViewData> mutableMap =
+      HashMultimap.<String, MutableViewData>create();
+
+  @GuardedBy("this")
+  private final Map<View.Name, View> registeredViews = new HashMap<View.Name, View>();
+
+  // TODO(songya): consider adding a Measure.Name class
+  @GuardedBy("this")
+  private final Map<String, Measure> registeredMeasures = Maps.newHashMap();
+
+  // Cached set of exported views. It must be set to null whenever a view is registered or
+  // unregistered.
+  @javax.annotation.Nullable private volatile Set<View> exportedViews;
+
+  /** Returns a {@link ViewData} corresponding to the given {@link View.Name}. */
+  @javax.annotation.Nullable
+  synchronized ViewData getView(View.Name viewName, Clock clock, State state) {
+    MutableViewData view = getMutableViewData(viewName);
+    return view == null ? null : view.toViewData(clock.now(), state);
+  }
+
+  Set<View> getExportedViews() {
+    Set<View> views = exportedViews;
+    if (views == null) {
+      synchronized (this) {
+        exportedViews = views = filterExportedViews(registeredViews.values());
+      }
+    }
+    return views;
+  }
+
+  // Returns the subset of the given views that should be exported
+  private static Set<View> filterExportedViews(Collection<View> allViews) {
+    Set<View> views = Sets.newHashSet();
+    for (View view : allViews) {
+      if (view.getWindow() instanceof View.AggregationWindow.Cumulative) {
+        views.add(view);
+      }
+    }
+    return Collections.unmodifiableSet(views);
+  }
+
+  /** Enable stats collection for the given {@link View}. */
+  synchronized void registerView(View view, Clock clock) {
+    exportedViews = null;
+    View existing = registeredViews.get(view.getName());
+    if (existing != null) {
+      if (existing.equals(view)) {
+        // Ignore views that are already registered.
+        return;
+      } else {
+        throw new IllegalArgumentException(
+            "A different view with the same name is already registered: " + existing);
+      }
+    }
+    Measure measure = view.getMeasure();
+    Measure registeredMeasure = registeredMeasures.get(measure.getName());
+    if (registeredMeasure != null && !registeredMeasure.equals(measure)) {
+      throw new IllegalArgumentException(
+          "A different measure with the same name is already registered: " + registeredMeasure);
+    }
+    registeredViews.put(view.getName(), view);
+    if (registeredMeasure == null) {
+      registeredMeasures.put(measure.getName(), measure);
+    }
+    Timestamp now = clock.now();
+    mutableMap.put(view.getMeasure().getName(), MutableViewData.create(view, now));
+  }
+
+  @javax.annotation.Nullable
+  private synchronized MutableViewData getMutableViewData(View.Name viewName) {
+    View view = registeredViews.get(viewName);
+    if (view == null) {
+      return null;
+    }
+    Collection<MutableViewData> views = mutableMap.get(view.getMeasure().getName());
+    for (MutableViewData viewData : views) {
+      if (viewData.getView().getName().equals(viewName)) {
+        return viewData;
+      }
+    }
+    throw new AssertionError(
+        "Internal error: Not recording stats for view: \""
+            + viewName
+            + "\" registeredViews="
+            + registeredViews
+            + ", mutableMap="
+            + mutableMap);
+  }
+
+  // Records stats with a set of tags.
+  synchronized void record(TagContext tags, MeasureMapInternal stats, Timestamp timestamp) {
+    Iterator<Measurement> iterator = stats.iterator();
+    Map<String, String> attachments = stats.getAttachments();
+    while (iterator.hasNext()) {
+      Measurement measurement = iterator.next();
+      Measure measure = measurement.getMeasure();
+      if (!measure.equals(registeredMeasures.get(measure.getName()))) {
+        // unregistered measures will be ignored.
+        continue;
+      }
+      Collection<MutableViewData> viewDataCollection = mutableMap.get(measure.getName());
+      for (MutableViewData viewData : viewDataCollection) {
+        viewData.record(
+            tags, RecordUtils.getDoubleValueFromMeasurement(measurement), timestamp, attachments);
+      }
+    }
+  }
+
+  synchronized List<Metric> getMetrics(Clock clock, State state) {
+    List<Metric> metrics = new ArrayList<Metric>();
+    Timestamp now = clock.now();
+    for (Entry<String, MutableViewData> entry : mutableMap.entries()) {
+      Metric metric = entry.getValue().toMetric(now, state);
+      if (metric != null) {
+        metrics.add(metric);
+      }
+    }
+    return metrics;
+  }
+
+  // Clear stats for all the current MutableViewData
+  synchronized void clearStats() {
+    for (Entry<String, Collection<MutableViewData>> entry : mutableMap.asMap().entrySet()) {
+      for (MutableViewData mutableViewData : entry.getValue()) {
+        mutableViewData.clearStats();
+      }
+    }
+  }
+
+  // Resume stats collection for all MutableViewData.
+  synchronized void resumeStatsCollection(Timestamp now) {
+    for (Entry<String, Collection<MutableViewData>> entry : mutableMap.asMap().entrySet()) {
+      for (MutableViewData mutableViewData : entry.getValue()) {
+        mutableViewData.resumeStatsCollection(now);
+      }
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MetricProducerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MetricProducerImpl.java
new file mode 100644
index 0000000..7bf9257
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MetricProducerImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018, 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.implcore.stats;
+
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricProducer;
+import java.util.Collection;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Implementation of {@link MetricProducer}. */
+@ThreadSafe
+final class MetricProducerImpl extends MetricProducer {
+
+  private final StatsManager statsManager;
+
+  MetricProducerImpl(StatsManager statsManager) {
+    this.statsManager = statsManager;
+  }
+
+  @Override
+  public Collection<Metric> getMetrics() {
+    return statsManager.getMetrics();
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MetricUtils.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MetricUtils.java
new file mode 100644
index 0000000..0dfb1d2
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MetricUtils.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018, 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.implcore.stats;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.List;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+@SuppressWarnings("deprecation")
+// Utils to convert Stats data models to Metric data models.
+final class MetricUtils {
+
+  @javax.annotation.Nullable
+  static MetricDescriptor viewToMetricDescriptor(View view) {
+    if (view.getWindow() instanceof View.AggregationWindow.Interval) {
+      // Only creates Metric for cumulative stats.
+      return null;
+    }
+    List<LabelKey> labelKeys = new ArrayList<LabelKey>();
+    for (TagKey tagKey : view.getColumns()) {
+      // TODO: add description
+      labelKeys.add(LabelKey.create(tagKey.getName(), ""));
+    }
+    Measure measure = view.getMeasure();
+    return MetricDescriptor.create(
+        view.getName().asString(),
+        view.getDescription(),
+        measure.getUnit(),
+        getType(measure, view.getAggregation()),
+        labelKeys);
+  }
+
+  @VisibleForTesting
+  static Type getType(Measure measure, Aggregation aggregation) {
+    return aggregation.match(
+        Functions.returnConstant(
+            measure.match(
+                TYPE_CUMULATIVE_DOUBLE_FUNCTION, // Sum Double
+                TYPE_CUMULATIVE_INT64_FUNCTION, // Sum Int64
+                TYPE_UNRECOGNIZED_FUNCTION)),
+        TYPE_CUMULATIVE_INT64_FUNCTION, // Count
+        TYPE_CUMULATIVE_DISTRIBUTION_FUNCTION, // Distribution
+        Functions.returnConstant(
+            measure.match(
+                TYPE_GAUGE_DOUBLE_FUNCTION, // LastValue Double
+                TYPE_GAUGE_INT64_FUNCTION, // LastValue Long
+                TYPE_UNRECOGNIZED_FUNCTION)),
+        AGGREGATION_TYPE_DEFAULT_FUNCTION);
+  }
+
+  static List<LabelValue> tagValuesToLabelValues(List</*@Nullable*/ TagValue> tagValues) {
+    List<LabelValue> labelValues = new ArrayList<LabelValue>();
+    for (/*@Nullable*/ TagValue tagValue : tagValues) {
+      labelValues.add(LabelValue.create(tagValue == null ? null : tagValue.asString()));
+    }
+    return labelValues;
+  }
+
+  private static final Function<Object, Type> TYPE_CUMULATIVE_DOUBLE_FUNCTION =
+      Functions.returnConstant(Type.CUMULATIVE_DOUBLE);
+
+  private static final Function<Object, Type> TYPE_CUMULATIVE_INT64_FUNCTION =
+      Functions.returnConstant(Type.CUMULATIVE_INT64);
+
+  private static final Function<Object, Type> TYPE_CUMULATIVE_DISTRIBUTION_FUNCTION =
+      Functions.returnConstant(Type.CUMULATIVE_DISTRIBUTION);
+
+  private static final Function<Object, Type> TYPE_GAUGE_DOUBLE_FUNCTION =
+      Functions.returnConstant(Type.GAUGE_DOUBLE);
+
+  private static final Function<Object, Type> TYPE_GAUGE_INT64_FUNCTION =
+      Functions.returnConstant(Type.GAUGE_INT64);
+
+  private static final Function<Object, Type> TYPE_UNRECOGNIZED_FUNCTION =
+      Functions.<Type>throwAssertionError();
+
+  private static final Function<Aggregation, Type> AGGREGATION_TYPE_DEFAULT_FUNCTION =
+      new Function<Aggregation, Type>() {
+        @Override
+        public Type apply(Aggregation arg) {
+          if (arg instanceof Aggregation.Mean) {
+            return Type.CUMULATIVE_DOUBLE; // Mean
+          }
+          throw new AssertionError();
+        }
+      };
+
+  private MetricUtils() {}
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MutableAggregation.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableAggregation.java
new file mode 100644
index 0000000..6e2bff1
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableAggregation.java
@@ -0,0 +1,556 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.export.Distribution;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.BucketBoundaries;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** Mutable version of {@link Aggregation} that supports adding values. */
+abstract class MutableAggregation {
+
+  private MutableAggregation() {}
+
+  // Tolerance for double comparison.
+  private static final double TOLERANCE = 1e-6;
+
+  /**
+   * Put a new value into the MutableAggregation.
+   *
+   * @param value new value to be added to population
+   * @param attachments the contextual information on an {@link Exemplar}
+   * @param timestamp the timestamp when the value is recorded
+   */
+  abstract void add(double value, Map<String, String> attachments, Timestamp timestamp);
+
+  // TODO(songya): remove this method once interval stats is completely removed.
+  /**
+   * Combine the internal values of this MutableAggregation and value of the given
+   * MutableAggregation, with the given fraction. Then set the internal value of this
+   * MutableAggregation to the combined value.
+   *
+   * @param other the other {@code MutableAggregation}. The type of this and other {@code
+   *     MutableAggregation} must match.
+   * @param fraction the fraction that the value in other {@code MutableAggregation} should
+   *     contribute. Must be within [0.0, 1.0].
+   */
+  abstract void combine(MutableAggregation other, double fraction);
+
+  abstract AggregationData toAggregationData();
+
+  abstract Point toPoint(Timestamp timestamp);
+
+  /** Calculate sum of doubles on aggregated {@code MeasureValue}s. */
+  static class MutableSumDouble extends MutableAggregation {
+
+    private double sum = 0.0;
+
+    private MutableSumDouble() {}
+
+    /**
+     * Construct a {@code MutableSumDouble}.
+     *
+     * @return an empty {@code MutableSumDouble}.
+     */
+    static MutableSumDouble create() {
+      return new MutableSumDouble();
+    }
+
+    @Override
+    void add(double value, Map<String, String> attachments, Timestamp timestamp) {
+      sum += value;
+    }
+
+    @Override
+    void combine(MutableAggregation other, double fraction) {
+      checkArgument(other instanceof MutableSumDouble, "MutableSumDouble expected.");
+      this.sum += fraction * ((MutableSumDouble) other).sum;
+    }
+
+    @Override
+    AggregationData toAggregationData() {
+      return AggregationData.SumDataDouble.create(sum);
+    }
+
+    @Override
+    Point toPoint(Timestamp timestamp) {
+      return Point.create(Value.doubleValue(sum), timestamp);
+    }
+
+    @VisibleForTesting
+    double getSum() {
+      return sum;
+    }
+  }
+
+  /** Calculate sum of longs on aggregated {@code MeasureValue}s. */
+  static final class MutableSumLong extends MutableSumDouble {
+    private MutableSumLong() {
+      super();
+    }
+
+    /**
+     * Construct a {@code MutableSumLong}.
+     *
+     * @return an empty {@code MutableSumLong}.
+     */
+    static MutableSumLong create() {
+      return new MutableSumLong();
+    }
+
+    @Override
+    AggregationData toAggregationData() {
+      return AggregationData.SumDataLong.create(Math.round(getSum()));
+    }
+
+    @Override
+    Point toPoint(Timestamp timestamp) {
+      return Point.create(Value.longValue(Math.round(getSum())), timestamp);
+    }
+  }
+
+  /** Calculate count on aggregated {@code MeasureValue}s. */
+  static final class MutableCount extends MutableAggregation {
+
+    private long count = 0;
+
+    private MutableCount() {}
+
+    /**
+     * Construct a {@code MutableCount}.
+     *
+     * @return an empty {@code MutableCount}.
+     */
+    static MutableCount create() {
+      return new MutableCount();
+    }
+
+    @Override
+    void add(double value, Map<String, String> attachments, Timestamp timestamp) {
+      count++;
+    }
+
+    @Override
+    void combine(MutableAggregation other, double fraction) {
+      checkArgument(other instanceof MutableCount, "MutableCount expected.");
+      this.count += Math.round(fraction * ((MutableCount) other).getCount());
+    }
+
+    @Override
+    AggregationData toAggregationData() {
+      return AggregationData.CountData.create(count);
+    }
+
+    @Override
+    Point toPoint(Timestamp timestamp) {
+      return Point.create(Value.longValue(count), timestamp);
+    }
+
+    /**
+     * Returns the aggregated count.
+     *
+     * @return the aggregated count.
+     */
+    long getCount() {
+      return count;
+    }
+  }
+
+  /** Calculate mean on aggregated {@code MeasureValue}s. */
+  static final class MutableMean extends MutableAggregation {
+
+    private double sum = 0.0;
+    private long count = 0;
+
+    private MutableMean() {}
+
+    /**
+     * Construct a {@code MutableMean}.
+     *
+     * @return an empty {@code MutableMean}.
+     */
+    static MutableMean create() {
+      return new MutableMean();
+    }
+
+    @Override
+    void add(double value, Map<String, String> attachments, Timestamp timestamp) {
+      count++;
+      sum += value;
+    }
+
+    @Override
+    void combine(MutableAggregation other, double fraction) {
+      checkArgument(other instanceof MutableMean, "MutableMean expected.");
+      MutableMean mutableMean = (MutableMean) other;
+      this.count += Math.round(mutableMean.count * fraction);
+      this.sum += mutableMean.sum * fraction;
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    AggregationData toAggregationData() {
+      return AggregationData.MeanData.create(getMean(), count);
+    }
+
+    @Override
+    Point toPoint(Timestamp timestamp) {
+      return Point.create(Value.doubleValue(getMean()), timestamp);
+    }
+
+    /**
+     * Returns the aggregated mean.
+     *
+     * @return the aggregated mean.
+     */
+    double getMean() {
+      return count == 0 ? 0 : sum / count;
+    }
+
+    /**
+     * Returns the aggregated count.
+     *
+     * @return the aggregated count.
+     */
+    long getCount() {
+      return count;
+    }
+
+    @VisibleForTesting
+    double getSum() {
+      return sum;
+    }
+  }
+
+  /** Calculate distribution stats on aggregated {@code MeasureValue}s. */
+  static final class MutableDistribution extends MutableAggregation {
+
+    private double sum = 0.0;
+    private double mean = 0.0;
+    private long count = 0;
+    private double sumOfSquaredDeviations = 0.0;
+
+    // Initial "impossible" values, that will get reset as soon as first value is added.
+    private double min = Double.POSITIVE_INFINITY;
+    private double max = Double.NEGATIVE_INFINITY;
+
+    private final BucketBoundaries bucketBoundaries;
+    private final long[] bucketCounts;
+
+    // If there's a histogram (i.e bucket boundaries are not empty) in this MutableDistribution,
+    // exemplars will have the same size to bucketCounts; otherwise exemplars are null.
+    // Only the newest exemplar will be kept at each index.
+    @javax.annotation.Nullable private final Exemplar[] exemplars;
+
+    private MutableDistribution(BucketBoundaries bucketBoundaries) {
+      this.bucketBoundaries = bucketBoundaries;
+      int buckets = bucketBoundaries.getBoundaries().size() + 1;
+      this.bucketCounts = new long[buckets];
+      // In the implementation, each histogram bucket can have up to one exemplar, and the exemplar
+      // array is guaranteed to be in ascending order.
+      // If there's no histogram, don't record exemplars.
+      this.exemplars = bucketBoundaries.getBoundaries().isEmpty() ? null : new Exemplar[buckets];
+    }
+
+    /**
+     * Construct a {@code MutableDistribution}.
+     *
+     * @return an empty {@code MutableDistribution}.
+     */
+    static MutableDistribution create(BucketBoundaries bucketBoundaries) {
+      checkNotNull(bucketBoundaries, "bucketBoundaries should not be null.");
+      return new MutableDistribution(bucketBoundaries);
+    }
+
+    @Override
+    void add(double value, Map<String, String> attachments, Timestamp timestamp) {
+      sum += value;
+      count++;
+
+      /*
+       * Update the sum of squared deviations from the mean with the given value. For values
+       * x_i this is Sum[i=1..n]((x_i - mean)^2)
+       *
+       * Computed using Welfords method (see
+       * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance, or Knuth, "The Art of
+       * Computer Programming", Vol. 2, page 323, 3rd edition)
+       */
+      double deltaFromMean = value - mean;
+      mean += deltaFromMean / count;
+      double deltaFromMean2 = value - mean;
+      sumOfSquaredDeviations += deltaFromMean * deltaFromMean2;
+
+      if (value < min) {
+        min = value;
+      }
+      if (value > max) {
+        max = value;
+      }
+
+      int bucket = 0;
+      for (; bucket < bucketBoundaries.getBoundaries().size(); bucket++) {
+        if (value < bucketBoundaries.getBoundaries().get(bucket)) {
+          break;
+        }
+      }
+      bucketCounts[bucket]++;
+
+      // No implicit recording for exemplars - if there are no attachments (contextual information),
+      // don't record exemplars.
+      if (!attachments.isEmpty() && exemplars != null) {
+        exemplars[bucket] = Exemplar.create(value, timestamp, attachments);
+      }
+    }
+
+    // We don't compute fractional MutableDistribution, it's either whole or none.
+    @Override
+    void combine(MutableAggregation other, double fraction) {
+      checkArgument(other instanceof MutableDistribution, "MutableDistribution expected.");
+      if (Math.abs(1.0 - fraction) > TOLERANCE) {
+        return;
+      }
+
+      MutableDistribution mutableDistribution = (MutableDistribution) other;
+      checkArgument(
+          this.bucketBoundaries.equals(mutableDistribution.bucketBoundaries),
+          "Bucket boundaries should match.");
+
+      // Algorithm for calculating the combination of sum of squared deviations:
+      // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Parallel_algorithm.
+      if (this.count + mutableDistribution.count > 0) {
+        double delta = mutableDistribution.mean - this.mean;
+        this.sumOfSquaredDeviations =
+            this.sumOfSquaredDeviations
+                + mutableDistribution.sumOfSquaredDeviations
+                + Math.pow(delta, 2)
+                    * this.count
+                    * mutableDistribution.count
+                    / (this.count + mutableDistribution.count);
+      }
+
+      this.count += mutableDistribution.count;
+      this.sum += mutableDistribution.sum;
+      this.mean = this.sum / this.count;
+
+      if (mutableDistribution.min < this.min) {
+        this.min = mutableDistribution.min;
+      }
+      if (mutableDistribution.max > this.max) {
+        this.max = mutableDistribution.max;
+      }
+
+      long[] bucketCounts = mutableDistribution.getBucketCounts();
+      for (int i = 0; i < bucketCounts.length; i++) {
+        this.bucketCounts[i] += bucketCounts[i];
+      }
+
+      Exemplar[] otherExemplars = mutableDistribution.getExemplars();
+      if (exemplars != null && otherExemplars != null) {
+        for (int i = 0; i < otherExemplars.length; i++) {
+          Exemplar exemplar = otherExemplars[i];
+          // Assume other is always newer than this, because we combined interval buckets in time
+          // order.
+          // If there's a newer exemplar, overwrite current value.
+          if (exemplar != null) {
+            this.exemplars[i] = exemplar;
+          }
+        }
+      }
+    }
+
+    @Override
+    AggregationData toAggregationData() {
+      List<Long> boxedBucketCounts = new ArrayList<Long>();
+      for (long bucketCount : bucketCounts) {
+        boxedBucketCounts.add(bucketCount);
+      }
+      List<Exemplar> exemplarList = new ArrayList<Exemplar>();
+      if (exemplars != null) {
+        for (Exemplar exemplar : exemplars) {
+          if (exemplar != null) {
+            exemplarList.add(exemplar);
+          }
+        }
+      }
+      return DistributionData.create(
+          mean, count, min, max, sumOfSquaredDeviations, boxedBucketCounts, exemplarList);
+    }
+
+    @Override
+    Point toPoint(Timestamp timestamp) {
+      List<Distribution.Bucket> buckets = new ArrayList<Distribution.Bucket>();
+      for (int bucket = 0; bucket < bucketCounts.length; bucket++) {
+        long bucketCount = bucketCounts[bucket];
+        @javax.annotation.Nullable AggregationData.DistributionData.Exemplar exemplar = null;
+        if (exemplars != null) {
+          exemplar = exemplars[bucket];
+        }
+
+        Distribution.Bucket metricBucket;
+        if (exemplar != null) {
+          // Bucket with an Exemplar.
+          metricBucket =
+              Distribution.Bucket.create(
+                  bucketCount,
+                  Distribution.Exemplar.create(
+                      exemplar.getValue(), exemplar.getTimestamp(), exemplar.getAttachments()));
+        } else {
+          // Bucket with no Exemplar.
+          metricBucket = Distribution.Bucket.create(bucketCount);
+        }
+        buckets.add(metricBucket);
+      }
+
+      // TODO(mayurkale): Drop the first bucket when converting to metrics.
+      // Reason: In Stats API, bucket bounds begin with -infinity (first bucket is (-infinity, 0)).
+      BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBoundaries.getBoundaries());
+
+      return Point.create(
+          Value.distributionValue(
+              Distribution.create(
+                  count, mean * count, sumOfSquaredDeviations, bucketOptions, buckets)),
+          timestamp);
+    }
+
+    double getMean() {
+      return mean;
+    }
+
+    long getCount() {
+      return count;
+    }
+
+    double getMin() {
+      return min;
+    }
+
+    double getMax() {
+      return max;
+    }
+
+    // Returns the aggregated sum of squared deviations.
+    double getSumOfSquaredDeviations() {
+      return sumOfSquaredDeviations;
+    }
+
+    long[] getBucketCounts() {
+      return bucketCounts;
+    }
+
+    BucketBoundaries getBucketBoundaries() {
+      return bucketBoundaries;
+    }
+
+    @javax.annotation.Nullable
+    Exemplar[] getExemplars() {
+      return exemplars;
+    }
+  }
+
+  /** Calculate double last value on aggregated {@code MeasureValue}s. */
+  static class MutableLastValueDouble extends MutableAggregation {
+
+    // Initial value that will get reset as soon as first value is added.
+    private double lastValue = Double.NaN;
+    // TODO(songya): remove this once interval stats is completely removed.
+    private boolean initialized = false;
+
+    private MutableLastValueDouble() {}
+
+    /**
+     * Construct a {@code MutableLastValueDouble}.
+     *
+     * @return an empty {@code MutableLastValueDouble}.
+     */
+    static MutableLastValueDouble create() {
+      return new MutableLastValueDouble();
+    }
+
+    @Override
+    void add(double value, Map<String, String> attachments, Timestamp timestamp) {
+      lastValue = value;
+      // TODO(songya): remove this once interval stats is completely removed.
+      if (!initialized) {
+        initialized = true;
+      }
+    }
+
+    @Override
+    void combine(MutableAggregation other, double fraction) {
+      checkArgument(other instanceof MutableLastValueDouble, "MutableLastValueDouble expected.");
+      MutableLastValueDouble otherValue = (MutableLastValueDouble) other;
+      // Assume other is always newer than this, because we combined interval buckets in time order.
+      // If there's a newer value, overwrite current value.
+      this.lastValue = otherValue.initialized ? otherValue.getLastValue() : this.lastValue;
+    }
+
+    @Override
+    AggregationData toAggregationData() {
+      return AggregationData.LastValueDataDouble.create(lastValue);
+    }
+
+    @Override
+    Point toPoint(Timestamp timestamp) {
+      return Point.create(Value.doubleValue(lastValue), timestamp);
+    }
+
+    @VisibleForTesting
+    double getLastValue() {
+      return lastValue;
+    }
+  }
+
+  /** Calculate last long value on aggregated {@code MeasureValue}s. */
+  static final class MutableLastValueLong extends MutableLastValueDouble {
+    private MutableLastValueLong() {
+      super();
+    }
+
+    /**
+     * Construct a {@code MutableLastValueLong}.
+     *
+     * @return an empty {@code MutableLastValueLong}.
+     */
+    static MutableLastValueLong create() {
+      return new MutableLastValueLong();
+    }
+
+    @Override
+    AggregationData toAggregationData() {
+      return AggregationData.LastValueDataLong.create(Math.round(getLastValue()));
+    }
+
+    @Override
+    Point toPoint(Timestamp timestamp) {
+      return Point.create(Value.longValue(Math.round(getLastValue())), timestamp);
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MutableViewData.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableViewData.java
new file mode 100644
index 0000000..928675e
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableViewData.java
@@ -0,0 +1,464 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static io.opencensus.implcore.stats.RecordUtils.createAggregationMap;
+import static io.opencensus.implcore.stats.RecordUtils.createMutableAggregation;
+import static io.opencensus.implcore.stats.RecordUtils.getTagMap;
+import static io.opencensus.implcore.stats.RecordUtils.getTagValues;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.CheckerFrameworkUtils;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** A mutable version of {@link ViewData}, used for recording stats and start/end time. */
+@SuppressWarnings("deprecation")
+abstract class MutableViewData {
+
+  @VisibleForTesting static final Timestamp ZERO_TIMESTAMP = Timestamp.create(0, 0);
+
+  private final View view;
+
+  private MutableViewData(View view) {
+    this.view = view;
+  }
+
+  /**
+   * Constructs a new {@link MutableViewData}.
+   *
+   * @param view the {@code View} linked with this {@code MutableViewData}.
+   * @param start the start {@code Timestamp}.
+   * @return a {@code MutableViewData}.
+   */
+  static MutableViewData create(final View view, final Timestamp start) {
+    return view.getWindow()
+        .match(
+            new CreateCumulative(view, start),
+            new CreateInterval(view, start),
+            Functions.<MutableViewData>throwAssertionError());
+  }
+
+  /** The {@link View} associated with this {@link ViewData}. */
+  View getView() {
+    return view;
+  }
+
+  @javax.annotation.Nullable
+  abstract Metric toMetric(Timestamp now, State state);
+
+  /** Record stats with the given tags. */
+  abstract void record(
+      TagContext context, double value, Timestamp timestamp, Map<String, String> attachments);
+
+  /** Convert this {@link MutableViewData} to {@link ViewData}. */
+  abstract ViewData toViewData(Timestamp now, State state);
+
+  // Clear recorded stats.
+  abstract void clearStats();
+
+  // Resume stats collection, and reset Start Timestamp (for CumulativeMutableViewData), or refresh
+  // bucket list (for InternalMutableViewData).
+  abstract void resumeStatsCollection(Timestamp now);
+
+  private static final class CumulativeMutableViewData extends MutableViewData {
+
+    private Timestamp start;
+    private final Map<List</*@Nullable*/ TagValue>, MutableAggregation> tagValueAggregationMap =
+        Maps.newHashMap();
+    // Cache a MetricDescriptor to avoid converting View to MetricDescriptor in the future.
+    private final MetricDescriptor metricDescriptor;
+
+    private CumulativeMutableViewData(View view, Timestamp start) {
+      super(view);
+      this.start = start;
+      MetricDescriptor metricDescriptor = MetricUtils.viewToMetricDescriptor(view);
+      if (metricDescriptor == null) {
+        throw new AssertionError(
+            "Cumulative view should be converted to a non-null MetricDescriptor.");
+      } else {
+        this.metricDescriptor = metricDescriptor;
+      }
+    }
+
+    @javax.annotation.Nullable
+    @Override
+    Metric toMetric(Timestamp now, State state) {
+      if (state == State.DISABLED) {
+        return null;
+      }
+      Type type = metricDescriptor.getType();
+      @javax.annotation.Nullable
+      Timestamp startTime = type == Type.GAUGE_INT64 || type == Type.GAUGE_DOUBLE ? null : start;
+      List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>();
+      for (Entry<List</*@Nullable*/ TagValue>, MutableAggregation> entry :
+          tagValueAggregationMap.entrySet()) {
+        List<LabelValue> labelValues = MetricUtils.tagValuesToLabelValues(entry.getKey());
+        Point point = entry.getValue().toPoint(now);
+        timeSeriesList.add(TimeSeries.createWithOnePoint(labelValues, point, startTime));
+      }
+      return Metric.create(metricDescriptor, timeSeriesList);
+    }
+
+    @Override
+    void record(
+        TagContext context, double value, Timestamp timestamp, Map<String, String> attachments) {
+      List</*@Nullable*/ TagValue> tagValues =
+          getTagValues(getTagMap(context), super.view.getColumns());
+      if (!tagValueAggregationMap.containsKey(tagValues)) {
+        tagValueAggregationMap.put(
+            tagValues,
+            createMutableAggregation(super.view.getAggregation(), super.getView().getMeasure()));
+      }
+      tagValueAggregationMap.get(tagValues).add(value, attachments, timestamp);
+    }
+
+    @Override
+    ViewData toViewData(Timestamp now, State state) {
+      if (state == State.ENABLED) {
+        return ViewData.create(
+            super.view,
+            createAggregationMap(tagValueAggregationMap, super.view.getMeasure()),
+            ViewData.AggregationWindowData.CumulativeData.create(start, now));
+      } else {
+        // If Stats state is DISABLED, return an empty ViewData.
+        return ViewData.create(
+            super.view,
+            Collections.<List</*@Nullable*/ TagValue>, AggregationData>emptyMap(),
+            ViewData.AggregationWindowData.CumulativeData.create(ZERO_TIMESTAMP, ZERO_TIMESTAMP));
+      }
+    }
+
+    @Override
+    void clearStats() {
+      tagValueAggregationMap.clear();
+    }
+
+    @Override
+    void resumeStatsCollection(Timestamp now) {
+      start = now;
+    }
+  }
+
+  /*
+   * For each IntervalView, we always keep a queue of N + 1 buckets (by default N is 4).
+   * Each bucket has a duration which is interval duration / N.
+   * Ideally:
+   * 1. the buckets should always be up-to-date,
+   * 2. current time should always be within the latest bucket, currently recorded stats should fall
+   *    into the latest bucket,
+   * 3. there are always N buckets before the current one, which holds the stats in the past
+   *    interval duration.
+   *
+   * When getView() is called, we will extract and combine the stats from the current and past
+   * buckets (part of the stats from the oldest bucket could have expired).
+   *
+   * However, in reality, we couldn't track the status of buckets all the time (keep monitoring and
+   * updating the bucket queue will be expensive). When we call record() or getView(), some or all
+   * of the buckets might be outdated, and we will need to "pad" new buckets to the queue and remove
+   * outdated ones. After refreshing buckets, the bucket queue will able to maintain the three
+   * invariants in the ideal situation.
+   *
+   * For example:
+   * 1. We have an IntervalView which has a duration of 8 seconds, we register this view at 10s.
+   * 2. Initially there will be 5 buckets: [2.0, 4.0), [4.0, 6.0), ..., [10.0, 12.0).
+   * 3. If users don't call record() or getView(), bucket queue will remain as it is, and some
+   *    buckets could expire.
+   * 4. Suppose record() is called at 15s, now we need to refresh the bucket queue. We need to add
+   *    two new buckets [12.0, 14.0) and [14.0, 16.0), and remove two expired buckets [2.0, 4.0)
+   *    and [4.0, 6.0)
+   * 5. Suppose record() is called again at 30s, all the current buckets should have expired. We add
+   *    5 new buckets [22.0, 24.0) ... [30.0, 32.0) and remove all the previous buckets.
+   * 6. Suppose users call getView() at 35s, again we need to add two new buckets and remove two
+   *    expired one, so that bucket queue is up-to-date. Now we combine stats from all buckets and
+   *    return the combined IntervalViewData.
+   */
+  private static final class IntervalMutableViewData extends MutableViewData {
+
+    // TODO(songya): allow customizable bucket size in the future.
+    private static final int N = 4; // IntervalView has N + 1 buckets
+
+    private final ArrayDeque<IntervalBucket> buckets = new ArrayDeque<IntervalBucket>();
+
+    private final Duration totalDuration; // Duration of the whole interval.
+    private final Duration bucketDuration; // Duration of a single bucket (totalDuration / N)
+
+    private IntervalMutableViewData(View view, Timestamp start) {
+      super(view);
+      Duration totalDuration = ((View.AggregationWindow.Interval) view.getWindow()).getDuration();
+      this.totalDuration = totalDuration;
+      this.bucketDuration = Duration.fromMillis(totalDuration.toMillis() / N);
+
+      // When initializing. add N empty buckets prior to the start timestamp of this
+      // IntervalMutableViewData, so that the last bucket will be the current one in effect.
+      shiftBucketList(N + 1, start);
+    }
+
+    @javax.annotation.Nullable
+    @Override
+    Metric toMetric(Timestamp now, State state) {
+      return null;
+    }
+
+    @Override
+    void record(
+        TagContext context, double value, Timestamp timestamp, Map<String, String> attachments) {
+      List</*@Nullable*/ TagValue> tagValues =
+          getTagValues(getTagMap(context), super.view.getColumns());
+      refreshBucketList(timestamp);
+      // It is always the last bucket that does the recording.
+      CheckerFrameworkUtils.castNonNull(buckets.peekLast())
+          .record(tagValues, value, attachments, timestamp);
+    }
+
+    @Override
+    ViewData toViewData(Timestamp now, State state) {
+      refreshBucketList(now);
+      if (state == State.ENABLED) {
+        return ViewData.create(
+            super.view,
+            combineBucketsAndGetAggregationMap(now),
+            ViewData.AggregationWindowData.IntervalData.create(now));
+      } else {
+        // If Stats state is DISABLED, return an empty ViewData.
+        return ViewData.create(
+            super.view,
+            Collections.<List</*@Nullable*/ TagValue>, AggregationData>emptyMap(),
+            ViewData.AggregationWindowData.IntervalData.create(ZERO_TIMESTAMP));
+      }
+    }
+
+    @Override
+    void clearStats() {
+      for (IntervalBucket bucket : buckets) {
+        bucket.clearStats();
+      }
+    }
+
+    @Override
+    void resumeStatsCollection(Timestamp now) {
+      // Refresh bucket list to be ready for stats recording, so that if record() is called right
+      // after stats state is turned back on, record() will be faster.
+      refreshBucketList(now);
+    }
+
+    // Add new buckets and remove expired buckets by comparing the current timestamp with
+    // timestamp of the last bucket.
+    private void refreshBucketList(Timestamp now) {
+      if (buckets.size() != N + 1) {
+        throw new AssertionError("Bucket list must have exactly " + (N + 1) + " buckets.");
+      }
+      Timestamp startOfLastBucket =
+          CheckerFrameworkUtils.castNonNull(buckets.peekLast()).getStart();
+      // TODO(songya): decide what to do when time goes backwards
+      checkArgument(
+          now.compareTo(startOfLastBucket) >= 0,
+          "Current time must be within or after the last bucket.");
+      long elapsedTimeMillis = now.subtractTimestamp(startOfLastBucket).toMillis();
+      long numOfPadBuckets = elapsedTimeMillis / bucketDuration.toMillis();
+
+      shiftBucketList(numOfPadBuckets, now);
+    }
+
+    // Add specified number of new buckets, and remove expired buckets
+    private void shiftBucketList(long numOfPadBuckets, Timestamp now) {
+      Timestamp startOfNewBucket;
+
+      if (!buckets.isEmpty()) {
+        startOfNewBucket =
+            CheckerFrameworkUtils.castNonNull(buckets.peekLast())
+                .getStart()
+                .addDuration(bucketDuration);
+      } else {
+        // Initialize bucket list. Should only enter this block once.
+        startOfNewBucket = subtractDuration(now, totalDuration);
+      }
+
+      if (numOfPadBuckets > N + 1) {
+        // All current buckets expired, need to add N + 1 new buckets. The start time of the latest
+        // bucket will be current time.
+        startOfNewBucket = subtractDuration(now, totalDuration);
+        numOfPadBuckets = N + 1;
+      }
+
+      for (int i = 0; i < numOfPadBuckets; i++) {
+        buckets.add(
+            new IntervalBucket(
+                startOfNewBucket,
+                bucketDuration,
+                super.view.getAggregation(),
+                super.view.getMeasure()));
+        startOfNewBucket = startOfNewBucket.addDuration(bucketDuration);
+      }
+
+      // removed expired buckets
+      while (buckets.size() > N + 1) {
+        buckets.pollFirst();
+      }
+    }
+
+    // Combine stats within each bucket, aggregate stats by tag values, and return the mapping from
+    // tag values to aggregation data.
+    private Map<List</*@Nullable*/ TagValue>, AggregationData> combineBucketsAndGetAggregationMap(
+        Timestamp now) {
+      // Need to maintain the order of inserted MutableAggregations (inserted based on time order).
+      Multimap<List</*@Nullable*/ TagValue>, MutableAggregation> multimap =
+          LinkedHashMultimap.create();
+
+      ArrayDeque<IntervalBucket> shallowCopy = new ArrayDeque<IntervalBucket>(buckets);
+
+      Aggregation aggregation = super.view.getAggregation();
+      Measure measure = super.view.getMeasure();
+      putBucketsIntoMultiMap(shallowCopy, multimap, aggregation, measure, now);
+      Map<List</*@Nullable*/ TagValue>, MutableAggregation> singleMap =
+          aggregateOnEachTagValueList(multimap, aggregation, measure);
+      return createAggregationMap(singleMap, super.getView().getMeasure());
+    }
+
+    // Put stats within each bucket to a multimap. Each tag value list (map key) could have multiple
+    // mutable aggregations (map value) from different buckets.
+    private static void putBucketsIntoMultiMap(
+        ArrayDeque<IntervalBucket> buckets,
+        Multimap<List</*@Nullable*/ TagValue>, MutableAggregation> multimap,
+        Aggregation aggregation,
+        Measure measure,
+        Timestamp now) {
+      // Put fractional stats of the head (oldest) bucket.
+      IntervalBucket head = CheckerFrameworkUtils.castNonNull(buckets.peekFirst());
+      IntervalBucket tail = CheckerFrameworkUtils.castNonNull(buckets.peekLast());
+      double fractionTail = tail.getFraction(now);
+      // TODO(songya): decide what to do when time goes backwards
+      checkArgument(
+          0.0 <= fractionTail && fractionTail <= 1.0,
+          "Fraction " + fractionTail + " should be within [0.0, 1.0].");
+      double fractionHead = 1.0 - fractionTail;
+      putFractionalMutableAggregationsToMultiMap(
+          head.getTagValueAggregationMap(), multimap, aggregation, measure, fractionHead);
+
+      // Put whole data of other buckets.
+      boolean shouldSkipFirst = true;
+      for (IntervalBucket bucket : buckets) {
+        if (shouldSkipFirst) {
+          shouldSkipFirst = false;
+          continue; // skip the first bucket
+        }
+        for (Entry<List</*@Nullable*/ TagValue>, MutableAggregation> entry :
+            bucket.getTagValueAggregationMap().entrySet()) {
+          multimap.put(entry.getKey(), entry.getValue());
+        }
+      }
+    }
+
+    // Put stats within one bucket into multimap, multiplied by a given fraction.
+    private static <T> void putFractionalMutableAggregationsToMultiMap(
+        Map<T, MutableAggregation> mutableAggrMap,
+        Multimap<T, MutableAggregation> multimap,
+        Aggregation aggregation,
+        Measure measure,
+        double fraction) {
+      for (Entry<T, MutableAggregation> entry : mutableAggrMap.entrySet()) {
+        // Initially empty MutableAggregations.
+        MutableAggregation fractionalMutableAgg = createMutableAggregation(aggregation, measure);
+        fractionalMutableAgg.combine(entry.getValue(), fraction);
+        multimap.put(entry.getKey(), fractionalMutableAgg);
+      }
+    }
+
+    // For each tag value list (key of AggregationMap), combine mutable aggregations into one
+    // mutable aggregation, thus convert the multimap into a single map.
+    private static <T> Map<T, MutableAggregation> aggregateOnEachTagValueList(
+        Multimap<T, MutableAggregation> multimap, Aggregation aggregation, Measure measure) {
+      Map<T, MutableAggregation> map = Maps.newHashMap();
+      for (T tagValues : multimap.keySet()) {
+        // Initially empty MutableAggregations.
+        MutableAggregation combinedAggregation = createMutableAggregation(aggregation, measure);
+        for (MutableAggregation mutableAggregation : multimap.get(tagValues)) {
+          combinedAggregation.combine(mutableAggregation, 1.0);
+        }
+        map.put(tagValues, combinedAggregation);
+      }
+      return map;
+    }
+
+    // Subtract a Duration from a Timestamp, and return a new Timestamp.
+    private static Timestamp subtractDuration(Timestamp timestamp, Duration duration) {
+      return timestamp.addDuration(Duration.create(-duration.getSeconds(), -duration.getNanos()));
+    }
+  }
+
+  private static final class CreateCumulative
+      implements Function<View.AggregationWindow.Cumulative, MutableViewData> {
+    @Override
+    public MutableViewData apply(View.AggregationWindow.Cumulative arg) {
+      return new CumulativeMutableViewData(view, start);
+    }
+
+    private final View view;
+    private final Timestamp start;
+
+    private CreateCumulative(View view, Timestamp start) {
+      this.view = view;
+      this.start = start;
+    }
+  }
+
+  private static final class CreateInterval
+      implements Function<View.AggregationWindow.Interval, MutableViewData> {
+    @Override
+    public MutableViewData apply(View.AggregationWindow.Interval arg) {
+      return new IntervalMutableViewData(view, start);
+    }
+
+    private final View view;
+    private final Timestamp start;
+
+    private CreateInterval(View view, Timestamp start) {
+      this.view = view;
+      this.start = start;
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/RecordUtils.java b/impl_core/src/main/java/io/opencensus/implcore/stats/RecordUtils.java
new file mode 100644
index 0000000..fbb593f
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/RecordUtils.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2018, 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.implcore.stats;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.implcore.stats.MutableAggregation.MutableCount;
+import io.opencensus.implcore.stats.MutableAggregation.MutableDistribution;
+import io.opencensus.implcore.stats.MutableAggregation.MutableLastValueDouble;
+import io.opencensus.implcore.stats.MutableAggregation.MutableLastValueLong;
+import io.opencensus.implcore.stats.MutableAggregation.MutableMean;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSumDouble;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSumLong;
+import io.opencensus.implcore.tags.TagContextImpl;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.Measurement;
+import io.opencensus.stats.Measurement.MeasurementDouble;
+import io.opencensus.stats.Measurement.MeasurementLong;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+@SuppressWarnings("deprecation")
+/* Common static utilities for stats recording. */
+final class RecordUtils {
+
+  @javax.annotation.Nullable @VisibleForTesting static final TagValue UNKNOWN_TAG_VALUE = null;
+
+  static Map<TagKey, TagValue> getTagMap(TagContext ctx) {
+    if (ctx instanceof TagContextImpl) {
+      return ((TagContextImpl) ctx).getTags();
+    } else {
+      Map<TagKey, TagValue> tags = Maps.newHashMap();
+      for (Iterator<Tag> i = InternalUtils.getTags(ctx); i.hasNext(); ) {
+        Tag tag = i.next();
+        tags.put(tag.getKey(), tag.getValue());
+      }
+      return tags;
+    }
+  }
+
+  @VisibleForTesting
+  static List</*@Nullable*/ TagValue> getTagValues(
+      Map<? extends TagKey, ? extends TagValue> tags, List<? extends TagKey> columns) {
+    List</*@Nullable*/ TagValue> tagValues = new ArrayList</*@Nullable*/ TagValue>(columns.size());
+    // Record all the measures in a "Greedy" way.
+    // Every view aggregates every measure. This is similar to doing a GROUPBY view’s keys.
+    for (int i = 0; i < columns.size(); ++i) {
+      TagKey tagKey = columns.get(i);
+      if (!tags.containsKey(tagKey)) {
+        // replace not found key values by null.
+        tagValues.add(UNKNOWN_TAG_VALUE);
+      } else {
+        tagValues.add(tags.get(tagKey));
+      }
+    }
+    return tagValues;
+  }
+
+  /**
+   * Create an empty {@link MutableAggregation} based on the given {@link Aggregation}.
+   *
+   * @param aggregation {@code Aggregation}.
+   * @return an empty {@code MutableAggregation}.
+   */
+  @VisibleForTesting
+  static MutableAggregation createMutableAggregation(
+      Aggregation aggregation, final Measure measure) {
+    return aggregation.match(
+        new Function<Sum, MutableAggregation>() {
+          @Override
+          public MutableAggregation apply(Sum arg) {
+            return measure.match(
+                CreateMutableSumDouble.INSTANCE,
+                CreateMutableSumLong.INSTANCE,
+                Functions.<MutableAggregation>throwAssertionError());
+          }
+        },
+        CreateMutableCount.INSTANCE,
+        CreateMutableDistribution.INSTANCE,
+        new Function<LastValue, MutableAggregation>() {
+          @Override
+          public MutableAggregation apply(LastValue arg) {
+            return measure.match(
+                CreateMutableLastValueDouble.INSTANCE,
+                CreateMutableLastValueLong.INSTANCE,
+                Functions.<MutableAggregation>throwAssertionError());
+          }
+        },
+        AggregationDefaultFunction.INSTANCE);
+  }
+
+  // Covert a mapping from TagValues to MutableAggregation, to a mapping from TagValues to
+  // AggregationData.
+  static <T> Map<T, AggregationData> createAggregationMap(
+      Map<T, MutableAggregation> tagValueAggregationMap, Measure measure) {
+    Map<T, AggregationData> map = Maps.newHashMap();
+    for (Entry<T, MutableAggregation> entry : tagValueAggregationMap.entrySet()) {
+      map.put(entry.getKey(), entry.getValue().toAggregationData());
+    }
+    return map;
+  }
+
+  static double getDoubleValueFromMeasurement(Measurement measurement) {
+    return measurement.match(
+        GET_VALUE_FROM_MEASUREMENT_DOUBLE,
+        GET_VALUE_FROM_MEASUREMENT_LONG,
+        Functions.<Double>throwAssertionError());
+  }
+
+  // static inner Function classes
+
+  private static final Function<MeasurementDouble, Double> GET_VALUE_FROM_MEASUREMENT_DOUBLE =
+      new Function<MeasurementDouble, Double>() {
+        @Override
+        public Double apply(MeasurementDouble arg) {
+          return arg.getValue();
+        }
+      };
+
+  private static final Function<MeasurementLong, Double> GET_VALUE_FROM_MEASUREMENT_LONG =
+      new Function<MeasurementLong, Double>() {
+        @Override
+        public Double apply(MeasurementLong arg) {
+          // TODO: consider checking truncation here.
+          return (double) arg.getValue();
+        }
+      };
+
+  private static final class CreateMutableSumDouble
+      implements Function<MeasureDouble, MutableAggregation> {
+    @Override
+    public MutableAggregation apply(MeasureDouble arg) {
+      return MutableSumDouble.create();
+    }
+
+    private static final CreateMutableSumDouble INSTANCE = new CreateMutableSumDouble();
+  }
+
+  private static final class CreateMutableSumLong
+      implements Function<MeasureLong, MutableAggregation> {
+    @Override
+    public MutableAggregation apply(MeasureLong arg) {
+      return MutableSumLong.create();
+    }
+
+    private static final CreateMutableSumLong INSTANCE = new CreateMutableSumLong();
+  }
+
+  private static final class CreateMutableCount implements Function<Count, MutableAggregation> {
+    @Override
+    public MutableAggregation apply(Count arg) {
+      return MutableCount.create();
+    }
+
+    private static final CreateMutableCount INSTANCE = new CreateMutableCount();
+  }
+
+  // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+  // we need to continue supporting Mean, since it could still be used by users and some
+  // deprecated RPC views.
+  private static final class AggregationDefaultFunction
+      implements Function<Aggregation, MutableAggregation> {
+    @Override
+    public MutableAggregation apply(Aggregation arg) {
+      if (arg instanceof Aggregation.Mean) {
+        return MutableMean.create();
+      }
+      throw new IllegalArgumentException("Unknown Aggregation.");
+    }
+
+    private static final AggregationDefaultFunction INSTANCE = new AggregationDefaultFunction();
+  }
+
+  private static final class CreateMutableDistribution
+      implements Function<Distribution, MutableAggregation> {
+    @Override
+    public MutableAggregation apply(Distribution arg) {
+      return MutableDistribution.create(arg.getBucketBoundaries());
+    }
+
+    private static final CreateMutableDistribution INSTANCE = new CreateMutableDistribution();
+  }
+
+  private static final class CreateMutableLastValueDouble
+      implements Function<MeasureDouble, MutableAggregation> {
+    @Override
+    public MutableAggregation apply(MeasureDouble arg) {
+      return MutableLastValueDouble.create();
+    }
+
+    private static final CreateMutableLastValueDouble INSTANCE = new CreateMutableLastValueDouble();
+  }
+
+  private static final class CreateMutableLastValueLong
+      implements Function<MeasureLong, MutableAggregation> {
+    @Override
+    public MutableAggregation apply(MeasureLong arg) {
+      return MutableLastValueLong.create();
+    }
+
+    private static final CreateMutableLastValueLong INSTANCE = new CreateMutableLastValueLong();
+  }
+
+  private RecordUtils() {}
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/StatsComponentImplBase.java b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsComponentImplBase.java
new file mode 100644
index 0000000..741b399
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsComponentImplBase.java
@@ -0,0 +1,92 @@
+/*
+ * 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.implcore.stats;
+
+import com.google.common.base.Preconditions;
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.metrics.Metrics;
+import io.opencensus.metrics.export.MetricProducer;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.StatsComponent;
+
+/** Base implementation of {@link StatsComponent}. */
+public class StatsComponentImplBase extends StatsComponent {
+  private static final State DEFAULT_STATE = State.ENABLED;
+
+  // The State shared between the StatsComponent, StatsRecorder and ViewManager.
+  private final CurrentState currentState = new CurrentState(DEFAULT_STATE);
+
+  private final ViewManagerImpl viewManager;
+  private final StatsRecorderImpl statsRecorder;
+
+  /**
+   * Creates a new {@code StatsComponentImplBase}.
+   *
+   * @param queue the queue implementation.
+   * @param clock the clock to use when recording stats.
+   */
+  public StatsComponentImplBase(EventQueue queue, Clock clock) {
+    StatsManager statsManager = new StatsManager(queue, clock, currentState);
+    this.viewManager = new ViewManagerImpl(statsManager);
+    this.statsRecorder = new StatsRecorderImpl(statsManager);
+
+    // Create a new MetricProducerImpl and register it to MetricProducerManager when
+    // StatsComponentImplBase is initialized.
+    MetricProducer metricProducer = new MetricProducerImpl(statsManager);
+    Metrics.getExportComponent().getMetricProducerManager().add(metricProducer);
+  }
+
+  @Override
+  public ViewManagerImpl getViewManager() {
+    return viewManager;
+  }
+
+  @Override
+  public StatsRecorderImpl getStatsRecorder() {
+    return statsRecorder;
+  }
+
+  @Override
+  public StatsCollectionState getState() {
+    return stateToStatsState(currentState.get());
+  }
+
+  @Override
+  @SuppressWarnings("deprecation")
+  public synchronized void setState(StatsCollectionState newState) {
+    boolean stateChanged =
+        currentState.set(statsStateToState(Preconditions.checkNotNull(newState, "newState")));
+    if (stateChanged) {
+      if (newState == StatsCollectionState.DISABLED) {
+        viewManager.clearStats();
+      } else {
+        viewManager.resumeStatsCollection();
+      }
+    }
+  }
+
+  private static State statsStateToState(StatsCollectionState statsCollectionState) {
+    return statsCollectionState == StatsCollectionState.ENABLED ? State.ENABLED : State.DISABLED;
+  }
+
+  private static StatsCollectionState stateToStatsState(State state) {
+    return state == State.ENABLED ? StatsCollectionState.ENABLED : StatsCollectionState.DISABLED;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/StatsManager.java b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsManager.java
new file mode 100644
index 0000000..17e99d4
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsManager.java
@@ -0,0 +1,104 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagContext;
+import java.util.Collection;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/** Object that stores all views and stats. */
+final class StatsManager {
+
+  private final EventQueue queue;
+
+  // clock used throughout the stats implementation
+  private final Clock clock;
+
+  private final CurrentState state;
+  private final MeasureToViewMap measureToViewMap = new MeasureToViewMap();
+
+  StatsManager(EventQueue queue, Clock clock, CurrentState state) {
+    checkNotNull(queue, "EventQueue");
+    checkNotNull(clock, "Clock");
+    checkNotNull(state, "state");
+    this.queue = queue;
+    this.clock = clock;
+    this.state = state;
+  }
+
+  void registerView(View view) {
+    measureToViewMap.registerView(view, clock);
+  }
+
+  @Nullable
+  ViewData getView(View.Name viewName) {
+    return measureToViewMap.getView(viewName, clock, state.getInternal());
+  }
+
+  Set<View> getExportedViews() {
+    return measureToViewMap.getExportedViews();
+  }
+
+  void record(TagContext tags, MeasureMapInternal measurementValues) {
+    // TODO(songya): consider exposing No-op MeasureMap and use it when stats state is DISABLED, so
+    // that we don't need to create actual MeasureMapImpl.
+    if (state.getInternal() == State.ENABLED) {
+      queue.enqueue(new StatsEvent(this, tags, measurementValues));
+    }
+  }
+
+  Collection<Metric> getMetrics() {
+    return measureToViewMap.getMetrics(clock, state.getInternal());
+  }
+
+  void clearStats() {
+    measureToViewMap.clearStats();
+  }
+
+  void resumeStatsCollection() {
+    measureToViewMap.resumeStatsCollection(clock.now());
+  }
+
+  // An EventQueue entry that records the stats from one call to StatsManager.record(...).
+  private static final class StatsEvent implements EventQueue.Entry {
+    private final TagContext tags;
+    private final MeasureMapInternal stats;
+    private final StatsManager statsManager;
+
+    StatsEvent(StatsManager statsManager, TagContext tags, MeasureMapInternal stats) {
+      this.statsManager = statsManager;
+      this.tags = tags;
+      this.stats = stats;
+    }
+
+    @Override
+    public void process() {
+      // Add Timestamp to value after it went through the DisruptorQueue.
+      statsManager.measureToViewMap.record(tags, stats, statsManager.clock.now());
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/StatsRecorderImpl.java b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsRecorderImpl.java
new file mode 100644
index 0000000..f9ebea4
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsRecorderImpl.java
@@ -0,0 +1,36 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.stats.StatsRecorder;
+
+/** Implementation of {@link StatsRecorder}. */
+public final class StatsRecorderImpl extends StatsRecorder {
+  private final StatsManager statsManager;
+
+  StatsRecorderImpl(StatsManager statsManager) {
+    checkNotNull(statsManager, "StatsManager");
+    this.statsManager = statsManager;
+  }
+
+  @Override
+  public MeasureMapImpl newMeasureMap() {
+    return MeasureMapImpl.create(statsManager);
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/ViewManagerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/stats/ViewManagerImpl.java
new file mode 100644
index 0000000..20ea97f
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/ViewManagerImpl.java
@@ -0,0 +1,56 @@
+/*
+ * 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.implcore.stats;
+
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/** Implementation of {@link ViewManager}. */
+public final class ViewManagerImpl extends ViewManager {
+  private final StatsManager statsManager;
+
+  ViewManagerImpl(StatsManager statsManager) {
+    this.statsManager = statsManager;
+  }
+
+  @Override
+  public void registerView(View view) {
+    statsManager.registerView(view);
+  }
+
+  @Override
+  @Nullable
+  public ViewData getView(View.Name viewName) {
+    return statsManager.getView(viewName);
+  }
+
+  @Override
+  public Set<View> getAllExportedViews() {
+    return statsManager.getExportedViews();
+  }
+
+  void clearStats() {
+    statsManager.clearStats();
+  }
+
+  void resumeStatsCollection() {
+    statsManager.resumeStatsCollection();
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTagContextUtils.java b/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTagContextUtils.java
new file mode 100644
index 0000000..e6bb12f
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTagContextUtils.java
@@ -0,0 +1,71 @@
+/*
+ * 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.implcore.tags;
+
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.unsafe.ContextUtils;
+
+/**
+ * Utility methods for accessing the {@link TagContext} contained in the {@link io.grpc.Context}.
+ */
+final class CurrentTagContextUtils {
+
+  private CurrentTagContextUtils() {}
+
+  /**
+   * Returns the {@link TagContext} from the current context.
+   *
+   * @return the {@code TagContext} from the current context.
+   */
+  static TagContext getCurrentTagContext() {
+    return ContextUtils.TAG_CONTEXT_KEY.get();
+  }
+
+  /**
+   * Enters the scope of code where the given {@link TagContext} is in the current context and
+   * returns an object that represents that scope. The scope is exited when the returned object is
+   * closed.
+   *
+   * @param tags the {@code TagContext} to be set to the current context.
+   * @return an object that defines a scope where the given {@code TagContext} is set to the current
+   *     context.
+   */
+  static Scope withTagContext(TagContext tags) {
+    return new WithTagContext(tags);
+  }
+
+  private static final class WithTagContext implements Scope {
+
+    private final Context orig;
+
+    /**
+     * Constructs a new {@link WithTagContext}.
+     *
+     * @param tags the {@code TagContext} to be added to the current {@code Context}.
+     */
+    private WithTagContext(TagContext tags) {
+      orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, tags).attach();
+    }
+
+    @Override
+    public void close() {
+      Context.current().detach(orig);
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/NoopTagContextBuilder.java b/impl_core/src/main/java/io/opencensus/implcore/tags/NoopTagContextBuilder.java
new file mode 100644
index 0000000..eae54c5
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/NoopTagContextBuilder.java
@@ -0,0 +1,51 @@
+/*
+ * 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.implcore.tags;
+
+import io.opencensus.common.Scope;
+import io.opencensus.implcore.internal.NoopScope;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+
+/** {@link TagContextBuilder} that is used when tagging is disabled. */
+final class NoopTagContextBuilder extends TagContextBuilder {
+  static final NoopTagContextBuilder INSTANCE = new NoopTagContextBuilder();
+
+  private NoopTagContextBuilder() {}
+
+  @Override
+  public TagContextBuilder put(TagKey key, TagValue value) {
+    return this;
+  }
+
+  @Override
+  public TagContextBuilder remove(TagKey key) {
+    return this;
+  }
+
+  @Override
+  public TagContext build() {
+    return TagContextImpl.EMPTY;
+  }
+
+  @Override
+  public Scope buildScoped() {
+    return NoopScope.getInstance();
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextBuilderImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextBuilderImpl.java
new file mode 100644
index 0000000..a17198d
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextBuilderImpl.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.implcore.tags;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Scope;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.HashMap;
+import java.util.Map;
+
+final class TagContextBuilderImpl extends TagContextBuilder {
+  private final Map<TagKey, TagValue> tags;
+
+  TagContextBuilderImpl(Map<TagKey, TagValue> tags) {
+    this.tags = new HashMap<TagKey, TagValue>(tags);
+  }
+
+  TagContextBuilderImpl() {
+    this.tags = new HashMap<TagKey, TagValue>();
+  }
+
+  @Override
+  public TagContextBuilderImpl put(TagKey key, TagValue value) {
+    tags.put(checkNotNull(key, "key"), checkNotNull(value, "value"));
+    return this;
+  }
+
+  @Override
+  public TagContextBuilderImpl remove(TagKey key) {
+    tags.remove(checkNotNull(key, "key"));
+    return this;
+  }
+
+  @Override
+  public TagContextImpl build() {
+    return new TagContextImpl(tags);
+  }
+
+  @Override
+  public Scope buildScoped() {
+    return CurrentTagContextUtils.withTagContext(build());
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextImpl.java
new file mode 100644
index 0000000..f7a8ff8
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextImpl.java
@@ -0,0 +1,85 @@
+/*
+ * 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.implcore.tags;
+
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public final class TagContextImpl extends TagContext {
+
+  public static final TagContextImpl EMPTY =
+      new TagContextImpl(Collections.<TagKey, TagValue>emptyMap());
+
+  // The types of the TagKey and value must match for each entry.
+  private final Map<TagKey, TagValue> tags;
+
+  public TagContextImpl(Map<? extends TagKey, ? extends TagValue> tags) {
+    this.tags = Collections.unmodifiableMap(new HashMap<TagKey, TagValue>(tags));
+  }
+
+  public Map<TagKey, TagValue> getTags() {
+    return tags;
+  }
+
+  @Override
+  protected Iterator<Tag> getIterator() {
+    return new TagIterator(tags);
+  }
+
+  @Override
+  public boolean equals(@Nullable Object other) {
+    // Directly compare the tags when both objects are TagContextImpls, for efficiency.
+    if (other instanceof TagContextImpl) {
+      return getTags().equals(((TagContextImpl) other).getTags());
+    }
+    return super.equals(other);
+  }
+
+  private static final class TagIterator implements Iterator<Tag> {
+    Iterator<Map.Entry<TagKey, TagValue>> iterator;
+
+    TagIterator(Map<TagKey, TagValue> tags) {
+      iterator = tags.entrySet().iterator();
+    }
+
+    @Override
+    public boolean hasNext() {
+      return iterator.hasNext();
+    }
+
+    @Override
+    public Tag next() {
+      final Entry<TagKey, TagValue> next = iterator.next();
+      return Tag.create(next.getKey(), next.getValue());
+    }
+
+    @Override
+    public void remove() {
+      throw new UnsupportedOperationException("TagIterator.remove()");
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextUtils.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextUtils.java
new file mode 100644
index 0000000..5fbc505
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextUtils.java
@@ -0,0 +1,33 @@
+/*
+ * 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.implcore.tags;
+
+import io.opencensus.tags.Tag;
+
+final class TagContextUtils {
+  private TagContextUtils() {}
+
+  /**
+   * Add a {@code Tag} of any type to a builder.
+   *
+   * @param tag tag containing the key and value to set.
+   * @param builder the builder to update.
+   */
+  static void addTagToBuilder(Tag tag, TagContextBuilderImpl builder) {
+    builder.put(tag.getKey(), tag.getValue());
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TaggerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TaggerImpl.java
new file mode 100644
index 0000000..dcf9a1b
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TaggerImpl.java
@@ -0,0 +1,116 @@
+/*
+ * 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.implcore.tags;
+
+import io.opencensus.common.Scope;
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.implcore.internal.NoopScope;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.Tagger;
+import java.util.Iterator;
+
+/** Implementation of {@link Tagger}. */
+public final class TaggerImpl extends Tagger {
+  // All methods in this class use TagContextImpl and TagContextBuilderImpl. For example,
+  // withTagContext(...) always puts a TagContextImpl into scope, even if the argument is another
+  // TagContext subclass.
+
+  private final CurrentState state;
+
+  TaggerImpl(CurrentState state) {
+    this.state = state;
+  }
+
+  @Override
+  public TagContextImpl empty() {
+    return TagContextImpl.EMPTY;
+  }
+
+  @Override
+  public TagContextImpl getCurrentTagContext() {
+    return state.getInternal() == State.DISABLED
+        ? TagContextImpl.EMPTY
+        : toTagContextImpl(CurrentTagContextUtils.getCurrentTagContext());
+  }
+
+  @Override
+  public TagContextBuilder emptyBuilder() {
+    return state.getInternal() == State.DISABLED
+        ? NoopTagContextBuilder.INSTANCE
+        : new TagContextBuilderImpl();
+  }
+
+  @Override
+  public TagContextBuilder currentBuilder() {
+    return state.getInternal() == State.DISABLED
+        ? NoopTagContextBuilder.INSTANCE
+        : toBuilder(CurrentTagContextUtils.getCurrentTagContext());
+  }
+
+  @Override
+  public TagContextBuilder toBuilder(TagContext tags) {
+    return state.getInternal() == State.DISABLED
+        ? NoopTagContextBuilder.INSTANCE
+        : toTagContextBuilderImpl(tags);
+  }
+
+  @Override
+  public Scope withTagContext(TagContext tags) {
+    return state.getInternal() == State.DISABLED
+        ? NoopScope.getInstance()
+        : CurrentTagContextUtils.withTagContext(toTagContextImpl(tags));
+  }
+
+  private static TagContextImpl toTagContextImpl(TagContext tags) {
+    if (tags instanceof TagContextImpl) {
+      return (TagContextImpl) tags;
+    } else {
+      Iterator<Tag> i = InternalUtils.getTags(tags);
+      if (!i.hasNext()) {
+        return TagContextImpl.EMPTY;
+      }
+      TagContextBuilderImpl builder = new TagContextBuilderImpl();
+      while (i.hasNext()) {
+        Tag tag = i.next();
+        if (tag != null) {
+          TagContextUtils.addTagToBuilder(tag, builder);
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static TagContextBuilderImpl toTagContextBuilderImpl(TagContext tags) {
+    // Copy the tags more efficiently in the expected case, when the TagContext is a TagContextImpl.
+    if (tags instanceof TagContextImpl) {
+      return new TagContextBuilderImpl(((TagContextImpl) tags).getTags());
+    } else {
+      TagContextBuilderImpl builder = new TagContextBuilderImpl();
+      for (Iterator<Tag> i = InternalUtils.getTags(tags); i.hasNext(); ) {
+        Tag tag = i.next();
+        if (tag != null) {
+          TagContextUtils.addTagToBuilder(tag, builder);
+        }
+      }
+      return builder;
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TagsComponentImplBase.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TagsComponentImplBase.java
new file mode 100644
index 0000000..88c31ba
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagsComponentImplBase.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.implcore.tags;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.implcore.tags.propagation.TagPropagationComponentImpl;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagPropagationComponent;
+
+/** Base implementation of {@link TagsComponent}. */
+public class TagsComponentImplBase extends TagsComponent {
+  private static final State DEFAULT_STATE = State.ENABLED;
+
+  // The State shared between the TagsComponent, Tagger, and TagPropagationComponent
+  private final CurrentState currentState = new CurrentState(DEFAULT_STATE);
+
+  private final Tagger tagger = new TaggerImpl(currentState);
+  private final TagPropagationComponent tagPropagationComponent =
+      new TagPropagationComponentImpl(currentState);
+
+  @Override
+  public Tagger getTagger() {
+    return tagger;
+  }
+
+  @Override
+  public TagPropagationComponent getTagPropagationComponent() {
+    return tagPropagationComponent;
+  }
+
+  @Override
+  public TaggingState getState() {
+    return stateToTaggingState(currentState.get());
+  }
+
+  @Override
+  @Deprecated
+  public void setState(TaggingState newState) {
+    currentState.set(taggingStateToState(checkNotNull(newState, "newState")));
+  }
+
+  private static State taggingStateToState(TaggingState taggingState) {
+    return taggingState == TaggingState.ENABLED ? State.ENABLED : State.DISABLED;
+  }
+
+  private static TaggingState stateToTaggingState(State state) {
+    return state == State.ENABLED ? TaggingState.ENABLED : TaggingState.DISABLED;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/SerializationUtils.java b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/SerializationUtils.java
new file mode 100644
index 0000000..2daad95
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/SerializationUtils.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2016-17, 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.implcore.tags.propagation;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Charsets;
+import com.google.common.io.ByteArrayDataOutput;
+import com.google.common.io.ByteStreams;
+import io.opencensus.implcore.internal.VarInt;
+import io.opencensus.implcore.tags.TagContextImpl;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import io.opencensus.tags.propagation.TagContextSerializationException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Methods for serializing and deserializing {@link TagContext}s.
+ *
+ * <p>The format defined in this class is shared across all implementations of OpenCensus. It allows
+ * tags to propagate across requests.
+ *
+ * <p>OpenCensus tag context encoding:
+ *
+ * <ul>
+ *   <li>Tags are encoded in single byte sequence. The version 0 format is:
+ *   <li>{@code <version_id><encoded_tags>}
+ *   <li>{@code <version_id> == a single byte, value 0}
+ *   <li>{@code <encoded_tags> == (<tag_field_id><tag_encoding>)*}
+ *       <ul>
+ *         <li>{@code <tag_field_id>} == a single byte, value 0
+ *         <li>{@code <tag_encoding>}:
+ *             <ul>
+ *               <li>{@code <tag_key_len><tag_key><tag_val_len><tag_val>}
+ *                   <ul>
+ *                     <li>{@code <tag_key_len>} == varint encoded integer
+ *                     <li>{@code <tag_key>} == tag_key_len bytes comprising tag key name
+ *                     <li>{@code <tag_val_len>} == varint encoded integer
+ *                     <li>{@code <tag_val>} == tag_val_len bytes comprising UTF-8 string
+ *                   </ul>
+ *             </ul>
+ *       </ul>
+ * </ul>
+ */
+final class SerializationUtils {
+  private SerializationUtils() {}
+
+  @VisibleForTesting static final int VERSION_ID = 0;
+  @VisibleForTesting static final int TAG_FIELD_ID = 0;
+  // This size limit only applies to the bytes representing tag keys and values.
+  @VisibleForTesting static final int TAGCONTEXT_SERIALIZED_SIZE_LIMIT = 8192;
+
+  // Serializes a TagContext to the on-the-wire format.
+  // Encoded tags are of the form: <version_id><encoded_tags>
+  static byte[] serializeBinary(TagContext tags) throws TagContextSerializationException {
+    // Use a ByteArrayDataOutput to avoid needing to handle IOExceptions.
+    final ByteArrayDataOutput byteArrayDataOutput = ByteStreams.newDataOutput();
+    byteArrayDataOutput.write(VERSION_ID);
+    int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars.
+    for (Iterator<Tag> i = InternalUtils.getTags(tags); i.hasNext(); ) {
+      Tag tag = i.next();
+      totalChars += tag.getKey().getName().length();
+      totalChars += tag.getValue().asString().length();
+      encodeTag(tag, byteArrayDataOutput);
+    }
+    if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) {
+      throw new TagContextSerializationException(
+          "Size of TagContext exceeds the maximum serialized size "
+              + TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
+    }
+    return byteArrayDataOutput.toByteArray();
+  }
+
+  // Deserializes input to TagContext based on the binary format standard.
+  // The encoded tags are of the form: <version_id><encoded_tags>
+  static TagContextImpl deserializeBinary(byte[] bytes) throws TagContextDeserializationException {
+    try {
+      if (bytes.length == 0) {
+        // Does not allow empty byte array.
+        throw new TagContextDeserializationException("Input byte[] can not be empty.");
+      }
+
+      ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
+      int versionId = buffer.get();
+      if (versionId > VERSION_ID || versionId < 0) {
+        throw new TagContextDeserializationException(
+            "Wrong Version ID: " + versionId + ". Currently supports version up to: " + VERSION_ID);
+      }
+      return new TagContextImpl(parseTags(buffer));
+    } catch (BufferUnderflowException exn) {
+      throw new TagContextDeserializationException(exn.toString()); // byte array format error.
+    }
+  }
+
+  private static Map<TagKey, TagValue> parseTags(ByteBuffer buffer)
+      throws TagContextDeserializationException {
+    Map<TagKey, TagValue> tags = new HashMap<TagKey, TagValue>();
+    int limit = buffer.limit();
+    int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars.
+    while (buffer.position() < limit) {
+      int type = buffer.get();
+      if (type == TAG_FIELD_ID) {
+        TagKey key = createTagKey(decodeString(buffer));
+        TagValue val = createTagValue(key, decodeString(buffer));
+        totalChars += key.getName().length();
+        totalChars += val.asString().length();
+        tags.put(key, val);
+      } else {
+        // Stop parsing at the first unknown field ID, since there is no way to know its length.
+        // TODO(sebright): Consider storing the rest of the byte array in the TagContext.
+        break;
+      }
+    }
+    if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) {
+      throw new TagContextDeserializationException(
+          "Size of TagContext exceeds the maximum serialized size "
+              + TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
+    }
+    return tags;
+  }
+
+  // TODO(sebright): Consider exposing a TagKey name validation method to avoid needing to catch an
+  // IllegalArgumentException here.
+  private static final TagKey createTagKey(String name) throws TagContextDeserializationException {
+    try {
+      return TagKey.create(name);
+    } catch (IllegalArgumentException e) {
+      throw new TagContextDeserializationException("Invalid tag key: " + name, e);
+    }
+  }
+
+  // TODO(sebright): Consider exposing a TagValue validation method to avoid needing to catch
+  // an IllegalArgumentException here.
+  private static final TagValue createTagValue(TagKey key, String value)
+      throws TagContextDeserializationException {
+    try {
+      return TagValue.create(value);
+    } catch (IllegalArgumentException e) {
+      throw new TagContextDeserializationException(
+          "Invalid tag value for key " + key + ": " + value, e);
+    }
+  }
+
+  private static final void encodeTag(Tag tag, ByteArrayDataOutput byteArrayDataOutput) {
+    byteArrayDataOutput.write(TAG_FIELD_ID);
+    encodeString(tag.getKey().getName(), byteArrayDataOutput);
+    encodeString(tag.getValue().asString(), byteArrayDataOutput);
+  }
+
+  private static final void encodeString(String input, ByteArrayDataOutput byteArrayDataOutput) {
+    putVarInt(input.length(), byteArrayDataOutput);
+    byteArrayDataOutput.write(input.getBytes(Charsets.UTF_8));
+  }
+
+  private static final void putVarInt(int input, ByteArrayDataOutput byteArrayDataOutput) {
+    byte[] output = new byte[VarInt.varIntSize(input)];
+    VarInt.putVarInt(input, output, 0);
+    byteArrayDataOutput.write(output);
+  }
+
+  private static final String decodeString(ByteBuffer buffer) {
+    int length = VarInt.getVarInt(buffer);
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < length; i++) {
+      builder.append((char) buffer.get());
+    }
+    return builder.toString();
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImpl.java
new file mode 100644
index 0000000..5a25da5
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImpl.java
@@ -0,0 +1,49 @@
+/*
+ * 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.implcore.tags.propagation;
+
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.implcore.tags.TagContextImpl;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import io.opencensus.tags.propagation.TagContextSerializationException;
+
+final class TagContextBinarySerializerImpl extends TagContextBinarySerializer {
+  private static final byte[] EMPTY_BYTE_ARRAY = {};
+
+  private final CurrentState state;
+
+  TagContextBinarySerializerImpl(CurrentState state) {
+    this.state = state;
+  }
+
+  @Override
+  public byte[] toByteArray(TagContext tags) throws TagContextSerializationException {
+    return state.getInternal() == State.DISABLED
+        ? EMPTY_BYTE_ARRAY
+        : SerializationUtils.serializeBinary(tags);
+  }
+
+  @Override
+  public TagContext fromByteArray(byte[] bytes) throws TagContextDeserializationException {
+    return state.getInternal() == State.DISABLED
+        ? TagContextImpl.EMPTY
+        : SerializationUtils.deserializeBinary(bytes);
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagPropagationComponentImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagPropagationComponentImpl.java
new file mode 100644
index 0000000..9ba0da4
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagPropagationComponentImpl.java
@@ -0,0 +1,35 @@
+/*
+ * 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.implcore.tags.propagation;
+
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagPropagationComponent;
+
+/** Implementation of {@link TagPropagationComponent}. */
+public final class TagPropagationComponentImpl extends TagPropagationComponent {
+  private final TagContextBinarySerializer tagContextBinarySerializer;
+
+  public TagPropagationComponentImpl(CurrentState state) {
+    tagContextBinarySerializer = new TagContextBinarySerializerImpl(state);
+  }
+
+  @Override
+  public TagContextBinarySerializer getBinarySerializer() {
+    return tagContextBinarySerializer;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/NoRecordEventsSpanImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/NoRecordEventsSpanImpl.java
new file mode 100644
index 0000000..8a5f8e0
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/NoRecordEventsSpanImpl.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018, 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.implcore.trace;
+
+import com.google.common.base.Preconditions;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Status;
+import java.util.EnumSet;
+import java.util.Map;
+
+/** Implementation for the {@link Span} class that does not record trace events. */
+final class NoRecordEventsSpanImpl extends Span {
+
+  private static final EnumSet<Options> NOT_RECORD_EVENTS_SPAN_OPTIONS =
+      EnumSet.noneOf(Span.Options.class);
+
+  static NoRecordEventsSpanImpl create(SpanContext context) {
+    return new NoRecordEventsSpanImpl(context);
+  }
+
+  @Override
+  public void addAnnotation(String description, Map<String, AttributeValue> attributes) {
+    Preconditions.checkNotNull(description, "description");
+    Preconditions.checkNotNull(attributes, "attribute");
+  }
+
+  @Override
+  public void addAnnotation(Annotation annotation) {
+    Preconditions.checkNotNull(annotation, "annotation");
+  }
+
+  @Override
+  public void putAttribute(String key, AttributeValue value) {
+    Preconditions.checkNotNull(key, "key");
+    Preconditions.checkNotNull(value, "value");
+  }
+
+  @Override
+  public void putAttributes(Map<String, AttributeValue> attributes) {
+    Preconditions.checkNotNull(attributes, "attributes");
+  }
+
+  @Override
+  public void addMessageEvent(io.opencensus.trace.MessageEvent messageEvent) {
+    Preconditions.checkNotNull(messageEvent, "messageEvent");
+  }
+
+  @Override
+  public void addLink(Link link) {
+    Preconditions.checkNotNull(link, "link");
+  }
+
+  @Override
+  public void setStatus(Status status) {
+    Preconditions.checkNotNull(status, "status");
+  }
+
+  @Override
+  public void end(EndSpanOptions options) {
+    Preconditions.checkNotNull(options, "options");
+  }
+
+  private NoRecordEventsSpanImpl(SpanContext context) {
+    super(context, NOT_RECORD_EVENTS_SPAN_OPTIONS);
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/RecordEventsSpanImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/RecordEventsSpanImpl.java
new file mode 100644
index 0000000..af3545b
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/RecordEventsSpanImpl.java
@@ -0,0 +1,579 @@
+/*
+ * 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.implcore.trace;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.EvictingQueue;
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.CheckerFrameworkUtils;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.internal.ConcurrentIntrusiveList.Element;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+// TODO(hailongwen): remove the usage of `NetworkEvent` in the future.
+/** Implementation for the {@link Span} class that records trace events. */
+@ThreadSafe
+public final class RecordEventsSpanImpl extends Span implements Element<RecordEventsSpanImpl> {
+  private static final Logger logger = Logger.getLogger(Tracer.class.getName());
+
+  private static final EnumSet<Span.Options> RECORD_EVENTS_SPAN_OPTIONS =
+      EnumSet.of(Span.Options.RECORD_EVENTS);
+
+  // The parent SpanId of this span. Null if this is a root span.
+  @Nullable private final SpanId parentSpanId;
+  // True if the parent is on a different process.
+  @Nullable private final Boolean hasRemoteParent;
+  // Active trace params when the Span was created.
+  private final TraceParams traceParams;
+  // Handler called when the span starts and ends.
+  private final StartEndHandler startEndHandler;
+  // The displayed name of the span.
+  private final String name;
+  // The kind of the span.
+  @Nullable private final Kind kind;
+  // The clock used to get the time.
+  private final Clock clock;
+  // The time converter used to convert nano time to Timestamp. This is needed because Java has
+  // millisecond granularity for Timestamp and tracing events are recorded more often.
+  @Nullable private final TimestampConverter timestampConverter;
+  // The start time of the span.
+  private final long startNanoTime;
+  // Set of recorded attributes. DO NOT CALL any other method that changes the ordering of events.
+  @GuardedBy("this")
+  @Nullable
+  private AttributesWithCapacity attributes;
+  // List of recorded annotations.
+  @GuardedBy("this")
+  @Nullable
+  private TraceEvents<EventWithNanoTime<Annotation>> annotations;
+  // List of recorded network events.
+  @GuardedBy("this")
+  @Nullable
+  private TraceEvents<EventWithNanoTime<io.opencensus.trace.MessageEvent>> messageEvents;
+  // List of recorded links to parent and child spans.
+  @GuardedBy("this")
+  @Nullable
+  private TraceEvents<Link> links;
+  // The status of the span.
+  @GuardedBy("this")
+  @Nullable
+  private Status status;
+  // The end time of the span.
+  @GuardedBy("this")
+  private long endNanoTime;
+  // True if the span is ended.
+  @GuardedBy("this")
+  private boolean hasBeenEnded;
+
+  @GuardedBy("this")
+  private boolean sampleToLocalSpanStore;
+
+  // Pointers for the ConcurrentIntrusiveList$Element. Guarded by the ConcurrentIntrusiveList.
+  @Nullable private RecordEventsSpanImpl next = null;
+  @Nullable private RecordEventsSpanImpl prev = null;
+
+  /**
+   * Creates and starts a span with the given configuration.
+   *
+   * @param context supplies the trace_id and span_id for the newly started span.
+   * @param name the displayed name for the new span.
+   * @param parentSpanId the span_id of the parent span, or null if the new span is a root span.
+   * @param hasRemoteParent {@code true} if the parentContext is remote. {@code null} if this is a
+   *     root span.
+   * @param traceParams trace parameters like sampler and probability.
+   * @param startEndHandler handler called when the span starts and ends.
+   * @param timestampConverter null if the span is a root span or the parent is not sampled. If the
+   *     parent is sampled, we should use the same converter to ensure ordering between tracing
+   *     events.
+   * @param clock the clock used to get the time.
+   * @return a new and started span.
+   */
+  @VisibleForTesting
+  public static RecordEventsSpanImpl startSpan(
+      SpanContext context,
+      String name,
+      @Nullable Kind kind,
+      @Nullable SpanId parentSpanId,
+      @Nullable Boolean hasRemoteParent,
+      TraceParams traceParams,
+      StartEndHandler startEndHandler,
+      @Nullable TimestampConverter timestampConverter,
+      Clock clock) {
+    RecordEventsSpanImpl span =
+        new RecordEventsSpanImpl(
+            context,
+            name,
+            kind,
+            parentSpanId,
+            hasRemoteParent,
+            traceParams,
+            startEndHandler,
+            timestampConverter,
+            clock);
+    // Call onStart here instead of calling in the constructor to make sure the span is completely
+    // initialized.
+    startEndHandler.onStart(span);
+    return span;
+  }
+
+  /**
+   * Returns the name of the {@code Span}.
+   *
+   * @return the name of the {@code Span}.
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Returns the status of the {@code Span}. If not set defaults to {@link Status#OK}.
+   *
+   * @return the status of the {@code Span}.
+   */
+  public Status getStatus() {
+    synchronized (this) {
+      return getStatusWithDefault();
+    }
+  }
+
+  /**
+   * Returns the end nano time (see {@link System#nanoTime()}). If the current {@code Span} is not
+   * ended then returns {@link Clock#nowNanos()}.
+   *
+   * @return the end nano time.
+   */
+  public long getEndNanoTime() {
+    synchronized (this) {
+      return hasBeenEnded ? endNanoTime : clock.nowNanos();
+    }
+  }
+
+  /**
+   * Returns the latency of the {@code Span} in nanos. If still active then returns now() - start
+   * time.
+   *
+   * @return the latency of the {@code Span} in nanos.
+   */
+  public long getLatencyNs() {
+    synchronized (this) {
+      return hasBeenEnded ? endNanoTime - startNanoTime : clock.nowNanos() - startNanoTime;
+    }
+  }
+
+  /**
+   * Returns if the name of this {@code Span} must be register to the {@code SampledSpanStore}.
+   *
+   * @return if the name of this {@code Span} must be register to the {@code SampledSpanStore}.
+   */
+  public boolean getSampleToLocalSpanStore() {
+    synchronized (this) {
+      checkState(hasBeenEnded, "Running span does not have the SampleToLocalSpanStore set.");
+      return sampleToLocalSpanStore;
+    }
+  }
+
+  /**
+   * Returns the kind of this {@code Span}.
+   *
+   * @return the kind of this {@code Span}.
+   */
+  @Nullable
+  public Kind getKind() {
+    return kind;
+  }
+
+  /**
+   * Returns the {@code TimestampConverter} used by this {@code Span}.
+   *
+   * @return the {@code TimestampConverter} used by this {@code Span}.
+   */
+  @Nullable
+  TimestampConverter getTimestampConverter() {
+    return timestampConverter;
+  }
+
+  /**
+   * Returns an immutable representation of all the data from this {@code Span}.
+   *
+   * @return an immutable representation of all the data from this {@code Span}.
+   * @throws IllegalStateException if the Span doesn't have RECORD_EVENTS option.
+   */
+  public SpanData toSpanData() {
+    synchronized (this) {
+      SpanData.Attributes attributesSpanData =
+          attributes == null
+              ? SpanData.Attributes.create(Collections.<String, AttributeValue>emptyMap(), 0)
+              : SpanData.Attributes.create(attributes, attributes.getNumberOfDroppedAttributes());
+      SpanData.TimedEvents<Annotation> annotationsSpanData =
+          createTimedEvents(getInitializedAnnotations(), timestampConverter);
+      SpanData.TimedEvents<io.opencensus.trace.MessageEvent> messageEventsSpanData =
+          createTimedEvents(getInitializedNetworkEvents(), timestampConverter);
+      SpanData.Links linksSpanData =
+          links == null
+              ? SpanData.Links.create(Collections.<Link>emptyList(), 0)
+              : SpanData.Links.create(
+                  new ArrayList<Link>(links.events), links.getNumberOfDroppedEvents());
+      return SpanData.create(
+          getContext(),
+          parentSpanId,
+          hasRemoteParent,
+          name,
+          kind,
+          CheckerFrameworkUtils.castNonNull(timestampConverter).convertNanoTime(startNanoTime),
+          attributesSpanData,
+          annotationsSpanData,
+          messageEventsSpanData,
+          linksSpanData,
+          null, // Not supported yet.
+          hasBeenEnded ? getStatusWithDefault() : null,
+          hasBeenEnded
+              ? CheckerFrameworkUtils.castNonNull(timestampConverter).convertNanoTime(endNanoTime)
+              : null);
+    }
+  }
+
+  @Override
+  public void putAttribute(String key, AttributeValue value) {
+    Preconditions.checkNotNull(key, "key");
+    Preconditions.checkNotNull(value, "value");
+    synchronized (this) {
+      if (hasBeenEnded) {
+        logger.log(Level.FINE, "Calling putAttributes() on an ended Span.");
+        return;
+      }
+      getInitializedAttributes().putAttribute(key, value);
+    }
+  }
+
+  @Override
+  public void putAttributes(Map<String, AttributeValue> attributes) {
+    Preconditions.checkNotNull(attributes, "attributes");
+    synchronized (this) {
+      if (hasBeenEnded) {
+        logger.log(Level.FINE, "Calling putAttributes() on an ended Span.");
+        return;
+      }
+      getInitializedAttributes().putAttributes(attributes);
+    }
+  }
+
+  @Override
+  public void addAnnotation(String description, Map<String, AttributeValue> attributes) {
+    Preconditions.checkNotNull(description, "description");
+    Preconditions.checkNotNull(attributes, "attribute");
+    synchronized (this) {
+      if (hasBeenEnded) {
+        logger.log(Level.FINE, "Calling addAnnotation() on an ended Span.");
+        return;
+      }
+      getInitializedAnnotations()
+          .addEvent(
+              new EventWithNanoTime<Annotation>(
+                  clock.nowNanos(),
+                  Annotation.fromDescriptionAndAttributes(description, attributes)));
+    }
+  }
+
+  @Override
+  public void addAnnotation(Annotation annotation) {
+    Preconditions.checkNotNull(annotation, "annotation");
+    synchronized (this) {
+      if (hasBeenEnded) {
+        logger.log(Level.FINE, "Calling addAnnotation() on an ended Span.");
+        return;
+      }
+      getInitializedAnnotations()
+          .addEvent(new EventWithNanoTime<Annotation>(clock.nowNanos(), annotation));
+    }
+  }
+
+  @Override
+  public void addMessageEvent(io.opencensus.trace.MessageEvent messageEvent) {
+    Preconditions.checkNotNull(messageEvent, "messageEvent");
+    synchronized (this) {
+      if (hasBeenEnded) {
+        logger.log(Level.FINE, "Calling addNetworkEvent() on an ended Span.");
+        return;
+      }
+      getInitializedNetworkEvents()
+          .addEvent(
+              new EventWithNanoTime<io.opencensus.trace.MessageEvent>(
+                  clock.nowNanos(), checkNotNull(messageEvent, "networkEvent")));
+    }
+  }
+
+  @Override
+  public void addLink(Link link) {
+    Preconditions.checkNotNull(link, "link");
+    synchronized (this) {
+      if (hasBeenEnded) {
+        logger.log(Level.FINE, "Calling addLink() on an ended Span.");
+        return;
+      }
+      getInitializedLinks().addEvent(link);
+    }
+  }
+
+  @Override
+  public void setStatus(Status status) {
+    Preconditions.checkNotNull(status, "status");
+    synchronized (this) {
+      if (hasBeenEnded) {
+        logger.log(Level.FINE, "Calling setStatus() on an ended Span.");
+        return;
+      }
+      this.status = status;
+    }
+  }
+
+  @Override
+  public void end(EndSpanOptions options) {
+    Preconditions.checkNotNull(options, "options");
+    synchronized (this) {
+      if (hasBeenEnded) {
+        logger.log(Level.FINE, "Calling end() on an ended Span.");
+        return;
+      }
+      if (options.getStatus() != null) {
+        status = options.getStatus();
+      }
+      sampleToLocalSpanStore = options.getSampleToLocalSpanStore();
+      endNanoTime = clock.nowNanos();
+      hasBeenEnded = true;
+    }
+    startEndHandler.onEnd(this);
+  }
+
+  @GuardedBy("this")
+  private AttributesWithCapacity getInitializedAttributes() {
+    if (attributes == null) {
+      attributes = new AttributesWithCapacity(traceParams.getMaxNumberOfAttributes());
+    }
+    return attributes;
+  }
+
+  @GuardedBy("this")
+  private TraceEvents<EventWithNanoTime<Annotation>> getInitializedAnnotations() {
+    if (annotations == null) {
+      annotations =
+          new TraceEvents<EventWithNanoTime<Annotation>>(traceParams.getMaxNumberOfAnnotations());
+    }
+    return annotations;
+  }
+
+  @GuardedBy("this")
+  private TraceEvents<EventWithNanoTime<io.opencensus.trace.MessageEvent>>
+      getInitializedNetworkEvents() {
+    if (messageEvents == null) {
+      messageEvents =
+          new TraceEvents<EventWithNanoTime<io.opencensus.trace.MessageEvent>>(
+              traceParams.getMaxNumberOfMessageEvents());
+    }
+    return messageEvents;
+  }
+
+  @GuardedBy("this")
+  private TraceEvents<Link> getInitializedLinks() {
+    if (links == null) {
+      links = new TraceEvents<Link>(traceParams.getMaxNumberOfLinks());
+    }
+    return links;
+  }
+
+  @GuardedBy("this")
+  private Status getStatusWithDefault() {
+    return status == null ? Status.OK : status;
+  }
+
+  private static <T> SpanData.TimedEvents<T> createTimedEvents(
+      TraceEvents<EventWithNanoTime<T>> events, @Nullable TimestampConverter timestampConverter) {
+    if (events == null) {
+      return SpanData.TimedEvents.create(Collections.<TimedEvent<T>>emptyList(), 0);
+    }
+    List<TimedEvent<T>> eventsList = new ArrayList<TimedEvent<T>>(events.events.size());
+    for (EventWithNanoTime<T> networkEvent : events.events) {
+      eventsList.add(
+          networkEvent.toSpanDataTimedEvent(CheckerFrameworkUtils.castNonNull(timestampConverter)));
+    }
+    return SpanData.TimedEvents.create(eventsList, events.getNumberOfDroppedEvents());
+  }
+
+  @Override
+  @Nullable
+  public RecordEventsSpanImpl getNext() {
+    return next;
+  }
+
+  @Override
+  public void setNext(@Nullable RecordEventsSpanImpl element) {
+    next = element;
+  }
+
+  @Override
+  @Nullable
+  public RecordEventsSpanImpl getPrev() {
+    return prev;
+  }
+
+  @Override
+  public void setPrev(@Nullable RecordEventsSpanImpl element) {
+    prev = element;
+  }
+
+  /**
+   * Interface to handle the start and end operations for a {@link Span} only when the {@code Span}
+   * has {@link Options#RECORD_EVENTS} option.
+   *
+   * <p>Implementation must avoid high overhead work in any of the methods because the code is
+   * executed on the critical path.
+   *
+   * <p>One instance can be called by multiple threads in the same time, so the implementation must
+   * be thread-safe.
+   */
+  public interface StartEndHandler {
+    void onStart(RecordEventsSpanImpl span);
+
+    void onEnd(RecordEventsSpanImpl span);
+  }
+
+  // A map implementation with a fixed capacity that drops events when the map gets full. Eviction
+  // is based on the access order.
+  private static final class AttributesWithCapacity extends LinkedHashMap<String, AttributeValue> {
+    private final int capacity;
+    private int totalRecordedAttributes = 0;
+    // Here because -Werror complains about this: [serial] serializable class AttributesWithCapacity
+    // has no definition of serialVersionUID. This class shouldn't be serialized.
+    private static final long serialVersionUID = 42L;
+
+    private AttributesWithCapacity(int capacity) {
+      // Capacity of the map is capacity + 1 to avoid resizing because removeEldestEntry is invoked
+      // by put and putAll after inserting a new entry into the map. The loadFactor is set to 1
+      // to avoid resizing because. The accessOrder is set to true.
+      super(capacity + 1, 1, /*accessOrder=*/ true);
+      this.capacity = capacity;
+    }
+
+    // Users must call this method instead of put to keep count of the total number of entries
+    // inserted.
+    private void putAttribute(String key, AttributeValue value) {
+      totalRecordedAttributes += 1;
+      put(key, value);
+    }
+
+    // Users must call this method instead of putAll to keep count of the total number of entries
+    // inserted.
+    private void putAttributes(Map<String, AttributeValue> attributes) {
+      totalRecordedAttributes += attributes.size();
+      putAll(attributes);
+    }
+
+    private int getNumberOfDroppedAttributes() {
+      return totalRecordedAttributes - size();
+    }
+
+    // It is called after each put or putAll call in order to determine if the eldest inserted
+    // entry should be removed or not.
+    @Override
+    protected boolean removeEldestEntry(Map.Entry<String, AttributeValue> eldest) {
+      return size() > this.capacity;
+    }
+  }
+
+  private static final class TraceEvents<T> {
+    private int totalRecordedEvents = 0;
+    private final EvictingQueue<T> events;
+
+    private int getNumberOfDroppedEvents() {
+      return totalRecordedEvents - events.size();
+    }
+
+    TraceEvents(int maxNumEvents) {
+      events = EvictingQueue.create(maxNumEvents);
+    }
+
+    void addEvent(T event) {
+      totalRecordedEvents++;
+      events.add(event);
+    }
+  }
+
+  // Timed event that uses nanoTime to represent the Timestamp.
+  private static final class EventWithNanoTime<T> {
+    private final long nanoTime;
+    private final T event;
+
+    private EventWithNanoTime(long nanoTime, T event) {
+      this.nanoTime = nanoTime;
+      this.event = event;
+    }
+
+    private TimedEvent<T> toSpanDataTimedEvent(TimestampConverter timestampConverter) {
+      return TimedEvent.create(timestampConverter.convertNanoTime(nanoTime), event);
+    }
+  }
+
+  private RecordEventsSpanImpl(
+      SpanContext context,
+      String name,
+      @Nullable Kind kind,
+      @Nullable SpanId parentSpanId,
+      @Nullable Boolean hasRemoteParent,
+      TraceParams traceParams,
+      StartEndHandler startEndHandler,
+      @Nullable TimestampConverter timestampConverter,
+      Clock clock) {
+    super(context, RECORD_EVENTS_SPAN_OPTIONS);
+    this.parentSpanId = parentSpanId;
+    this.hasRemoteParent = hasRemoteParent;
+    this.name = name;
+    this.kind = kind;
+    this.traceParams = traceParams;
+    this.startEndHandler = startEndHandler;
+    this.clock = clock;
+    this.hasBeenEnded = false;
+    this.sampleToLocalSpanStore = false;
+    this.timestampConverter =
+        timestampConverter != null ? timestampConverter : TimestampConverter.now(clock);
+    startNanoTime = clock.nowNanos();
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/SpanBuilderImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/SpanBuilderImpl.java
new file mode 100644
index 0000000..5565e9d
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/SpanBuilderImpl.java
@@ -0,0 +1,253 @@
+/*
+ * 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.implcore.trace;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Link.Type;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.config.TraceParams;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import javax.annotation.Nullable;
+
+/** Implementation of the {@link SpanBuilder}. */
+final class SpanBuilderImpl extends SpanBuilder {
+  private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+
+  private static final TraceOptions SAMPLED_TRACE_OPTIONS =
+      TraceOptions.builder().setIsSampled(true).build();
+  private static final TraceOptions NOT_SAMPLED_TRACE_OPTIONS =
+      TraceOptions.builder().setIsSampled(false).build();
+
+  private final Options options;
+  private final String name;
+  @Nullable private final Span parent;
+  @Nullable private final SpanContext remoteParentSpanContext;
+  @Nullable private Sampler sampler;
+  private List<Span> parentLinks = Collections.<Span>emptyList();
+  @Nullable private Boolean recordEvents;
+  @Nullable private Kind kind;
+
+  private Span startSpanInternal(
+      @Nullable SpanContext parent,
+      @Nullable Boolean hasRemoteParent,
+      String name,
+      @Nullable Sampler sampler,
+      List<Span> parentLinks,
+      @Nullable Boolean recordEvents,
+      @Nullable Kind kind,
+      @Nullable TimestampConverter timestampConverter) {
+    TraceParams activeTraceParams = options.traceConfig.getActiveTraceParams();
+    Random random = options.randomHandler.current();
+    TraceId traceId;
+    SpanId spanId = SpanId.generateRandomId(random);
+    SpanId parentSpanId = null;
+    // TODO(bdrutu): Handle tracestate correctly not just propagate.
+    Tracestate tracestate = TRACESTATE_DEFAULT;
+    if (parent == null || !parent.isValid()) {
+      // New root span.
+      traceId = TraceId.generateRandomId(random);
+      // This is a root span so no remote or local parent.
+      hasRemoteParent = null;
+    } else {
+      // New child span.
+      traceId = parent.getTraceId();
+      parentSpanId = parent.getSpanId();
+      tracestate = parent.getTracestate();
+    }
+    TraceOptions traceOptions =
+        makeSamplingDecision(
+                parent,
+                hasRemoteParent,
+                name,
+                sampler,
+                parentLinks,
+                traceId,
+                spanId,
+                activeTraceParams)
+            ? SAMPLED_TRACE_OPTIONS
+            : NOT_SAMPLED_TRACE_OPTIONS;
+    Span span =
+        (traceOptions.isSampled() || Boolean.TRUE.equals(recordEvents))
+            ? RecordEventsSpanImpl.startSpan(
+                SpanContext.create(traceId, spanId, traceOptions, tracestate),
+                name,
+                kind,
+                parentSpanId,
+                hasRemoteParent,
+                activeTraceParams,
+                options.startEndHandler,
+                timestampConverter,
+                options.clock)
+            : NoRecordEventsSpanImpl.create(
+                SpanContext.create(traceId, spanId, traceOptions, tracestate));
+    linkSpans(span, parentLinks);
+    return span;
+  }
+
+  private static boolean makeSamplingDecision(
+      @Nullable SpanContext parent,
+      @Nullable Boolean hasRemoteParent,
+      String name,
+      @Nullable Sampler sampler,
+      List<Span> parentLinks,
+      TraceId traceId,
+      SpanId spanId,
+      TraceParams activeTraceParams) {
+    // If users set a specific sampler in the SpanBuilder, use it.
+    if (sampler != null) {
+      return sampler.shouldSample(parent, hasRemoteParent, traceId, spanId, name, parentLinks);
+    }
+    // Use the default sampler if this is a root Span or this is an entry point Span (has remote
+    // parent).
+    if (Boolean.TRUE.equals(hasRemoteParent) || parent == null || !parent.isValid()) {
+      return activeTraceParams
+          .getSampler()
+          .shouldSample(parent, hasRemoteParent, traceId, spanId, name, parentLinks);
+    }
+    // Parent is always different than null because otherwise we use the default sampler.
+    return parent.getTraceOptions().isSampled() || isAnyParentLinkSampled(parentLinks);
+  }
+
+  private static boolean isAnyParentLinkSampled(List<Span> parentLinks) {
+    for (Span parentLink : parentLinks) {
+      if (parentLink.getContext().getTraceOptions().isSampled()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static void linkSpans(Span span, List<Span> parentLinks) {
+    if (!parentLinks.isEmpty()) {
+      Link childLink = Link.fromSpanContext(span.getContext(), Type.CHILD_LINKED_SPAN);
+      for (Span linkedSpan : parentLinks) {
+        linkedSpan.addLink(childLink);
+        span.addLink(Link.fromSpanContext(linkedSpan.getContext(), Type.PARENT_LINKED_SPAN));
+      }
+    }
+  }
+
+  static SpanBuilderImpl createWithParent(String spanName, @Nullable Span parent, Options options) {
+    return new SpanBuilderImpl(spanName, null, parent, options);
+  }
+
+  static SpanBuilderImpl createWithRemoteParent(
+      String spanName, @Nullable SpanContext remoteParentSpanContext, Options options) {
+    return new SpanBuilderImpl(spanName, remoteParentSpanContext, null, options);
+  }
+
+  private SpanBuilderImpl(
+      String name,
+      @Nullable SpanContext remoteParentSpanContext,
+      @Nullable Span parent,
+      Options options) {
+    this.name = checkNotNull(name, "name");
+    this.parent = parent;
+    this.remoteParentSpanContext = remoteParentSpanContext;
+    this.options = options;
+  }
+
+  @Override
+  public Span startSpan() {
+    SpanContext parentContext = remoteParentSpanContext;
+    Boolean hasRemoteParent = Boolean.TRUE;
+    TimestampConverter timestampConverter = null;
+    if (remoteParentSpanContext == null) {
+      // This is not a child of a remote Span. Get the parent SpanContext from the parent Span if
+      // any.
+      Span parent = this.parent;
+      hasRemoteParent = Boolean.FALSE;
+      if (parent != null) {
+        parentContext = parent.getContext();
+        // Pass the timestamp converter from the parent to ensure that the recorded events are in
+        // the right order. Implementation uses System.nanoTime() which is monotonically increasing.
+        if (parent instanceof RecordEventsSpanImpl) {
+          timestampConverter = ((RecordEventsSpanImpl) parent).getTimestampConverter();
+        }
+      } else {
+        hasRemoteParent = null;
+      }
+    }
+    return startSpanInternal(
+        parentContext,
+        hasRemoteParent,
+        name,
+        sampler,
+        parentLinks,
+        recordEvents,
+        kind,
+        timestampConverter);
+  }
+
+  static final class Options {
+    private final RandomHandler randomHandler;
+    private final RecordEventsSpanImpl.StartEndHandler startEndHandler;
+    private final Clock clock;
+    private final TraceConfig traceConfig;
+
+    Options(
+        RandomHandler randomHandler,
+        RecordEventsSpanImpl.StartEndHandler startEndHandler,
+        Clock clock,
+        TraceConfig traceConfig) {
+      this.randomHandler = checkNotNull(randomHandler, "randomHandler");
+      this.startEndHandler = checkNotNull(startEndHandler, "startEndHandler");
+      this.clock = checkNotNull(clock, "clock");
+      this.traceConfig = checkNotNull(traceConfig, "traceConfig");
+    }
+  }
+
+  @Override
+  public SpanBuilderImpl setSampler(Sampler sampler) {
+    this.sampler = checkNotNull(sampler, "sampler");
+    return this;
+  }
+
+  @Override
+  public SpanBuilderImpl setParentLinks(List<Span> parentLinks) {
+    this.parentLinks = checkNotNull(parentLinks, "parentLinks");
+    return this;
+  }
+
+  @Override
+  public SpanBuilderImpl setRecordEvents(boolean recordEvents) {
+    this.recordEvents = recordEvents;
+    return this;
+  }
+
+  @Override
+  public SpanBuilderImpl setSpanKind(@Nullable Kind kind) {
+    this.kind = kind;
+    return this;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/StartEndHandlerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/StartEndHandlerImpl.java
new file mode 100644
index 0000000..6adaa20
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/StartEndHandlerImpl.java
@@ -0,0 +1,127 @@
+/*
+ * 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.implcore.trace;
+
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.export.RunningSpanStoreImpl;
+import io.opencensus.implcore.trace.export.SampledSpanStoreImpl;
+import io.opencensus.implcore.trace.export.SpanExporterImpl;
+import io.opencensus.trace.Span.Options;
+import io.opencensus.trace.export.SpanData;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Uses the provided {@link EventQueue} to defer processing/exporting of the {@link SpanData} to
+ * avoid impacting the critical path.
+ */
+@ThreadSafe
+public final class StartEndHandlerImpl implements StartEndHandler {
+  private final SpanExporterImpl spanExporter;
+  @Nullable private final RunningSpanStoreImpl runningSpanStore;
+  @Nullable private final SampledSpanStoreImpl sampledSpanStore;
+  private final EventQueue eventQueue;
+  // true if any of (runningSpanStore OR sampledSpanStore) are different than null, which
+  // means the spans with RECORD_EVENTS should be enqueued in the queue.
+  private final boolean enqueueEventForNonSampledSpans;
+
+  /**
+   * Constructs a new {@code StartEndHandlerImpl}.
+   *
+   * @param spanExporter the {@code SpanExporter} implementation.
+   * @param runningSpanStore the {@code RunningSpanStore} implementation.
+   * @param sampledSpanStore the {@code SampledSpanStore} implementation.
+   * @param eventQueue the event queue where all the events are enqueued.
+   */
+  public StartEndHandlerImpl(
+      SpanExporterImpl spanExporter,
+      @Nullable RunningSpanStoreImpl runningSpanStore,
+      @Nullable SampledSpanStoreImpl sampledSpanStore,
+      EventQueue eventQueue) {
+    this.spanExporter = spanExporter;
+    this.runningSpanStore = runningSpanStore;
+    this.sampledSpanStore = sampledSpanStore;
+    this.enqueueEventForNonSampledSpans = runningSpanStore != null || sampledSpanStore != null;
+    this.eventQueue = eventQueue;
+  }
+
+  @Override
+  public void onStart(RecordEventsSpanImpl span) {
+    if (span.getOptions().contains(Options.RECORD_EVENTS) && enqueueEventForNonSampledSpans) {
+      eventQueue.enqueue(new SpanStartEvent(span, runningSpanStore));
+    }
+  }
+
+  @Override
+  public void onEnd(RecordEventsSpanImpl span) {
+    if ((span.getOptions().contains(Options.RECORD_EVENTS) && enqueueEventForNonSampledSpans)
+        || span.getContext().getTraceOptions().isSampled()) {
+      eventQueue.enqueue(new SpanEndEvent(span, spanExporter, runningSpanStore, sampledSpanStore));
+    }
+  }
+
+  // An EventQueue entry that records the start of the span event.
+  private static final class SpanStartEvent implements EventQueue.Entry {
+    private final RecordEventsSpanImpl span;
+    @Nullable private final RunningSpanStoreImpl activeSpansExporter;
+
+    SpanStartEvent(RecordEventsSpanImpl span, @Nullable RunningSpanStoreImpl activeSpansExporter) {
+      this.span = span;
+      this.activeSpansExporter = activeSpansExporter;
+    }
+
+    @Override
+    public void process() {
+      if (activeSpansExporter != null) {
+        activeSpansExporter.onStart(span);
+      }
+    }
+  }
+
+  // An EventQueue entry that records the end of the span event.
+  private static final class SpanEndEvent implements EventQueue.Entry {
+    private final RecordEventsSpanImpl span;
+    @Nullable private final RunningSpanStoreImpl runningSpanStore;
+    private final SpanExporterImpl spanExporter;
+    @Nullable private final SampledSpanStoreImpl sampledSpanStore;
+
+    SpanEndEvent(
+        RecordEventsSpanImpl span,
+        SpanExporterImpl spanExporter,
+        @Nullable RunningSpanStoreImpl runningSpanStore,
+        @Nullable SampledSpanStoreImpl sampledSpanStore) {
+      this.span = span;
+      this.runningSpanStore = runningSpanStore;
+      this.spanExporter = spanExporter;
+      this.sampledSpanStore = sampledSpanStore;
+    }
+
+    @Override
+    public void process() {
+      if (span.getContext().getTraceOptions().isSampled()) {
+        spanExporter.addSpan(span);
+      }
+      if (runningSpanStore != null) {
+        runningSpanStore.onEnd(span);
+      }
+      if (sampledSpanStore != null) {
+        sampledSpanStore.considerForSampling(span);
+      }
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/TraceComponentImplBase.java b/impl_core/src/main/java/io/opencensus/implcore/trace/TraceComponentImplBase.java
new file mode 100644
index 0000000..c143243
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/TraceComponentImplBase.java
@@ -0,0 +1,90 @@
+/*
+ * 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.implcore.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.config.TraceConfigImpl;
+import io.opencensus.implcore.trace.export.ExportComponentImpl;
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import io.opencensus.implcore.trace.propagation.PropagationComponentImpl;
+import io.opencensus.trace.TraceComponent;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/**
+ * Helper class to allow sharing the code for all the {@link TraceComponent} implementations. This
+ * class cannot use inheritance because in version 0.5.* the constructor of the {@code
+ * TraceComponent} is package protected.
+ *
+ * <p>This can be changed back to inheritance when version 0.5.* is no longer supported.
+ */
+public final class TraceComponentImplBase {
+  private final ExportComponentImpl exportComponent;
+  private final PropagationComponent propagationComponent = new PropagationComponentImpl();
+  private final Clock clock;
+  private final TraceConfig traceConfig = new TraceConfigImpl();
+  private final Tracer tracer;
+
+  /**
+   * Creates a new {@code TraceComponentImplBase}.
+   *
+   * @param clock the clock to use throughout tracing.
+   * @param randomHandler the random number generator for generating trace and span IDs.
+   * @param eventQueue the queue implementation.
+   */
+  public TraceComponentImplBase(Clock clock, RandomHandler randomHandler, EventQueue eventQueue) {
+    this.clock = clock;
+    // TODO(bdrutu): Add a config/argument for supportInProcessStores.
+    if (eventQueue instanceof SimpleEventQueue) {
+      exportComponent = ExportComponentImpl.createWithoutInProcessStores(eventQueue);
+    } else {
+      exportComponent = ExportComponentImpl.createWithInProcessStores(eventQueue);
+    }
+    StartEndHandler startEndHandler =
+        new StartEndHandlerImpl(
+            exportComponent.getSpanExporter(),
+            exportComponent.getRunningSpanStore(),
+            exportComponent.getSampledSpanStore(),
+            eventQueue);
+    tracer = new TracerImpl(randomHandler, startEndHandler, clock, traceConfig);
+  }
+
+  public Tracer getTracer() {
+    return tracer;
+  }
+
+  public PropagationComponent getPropagationComponent() {
+    return propagationComponent;
+  }
+
+  public final Clock getClock() {
+    return clock;
+  }
+
+  public ExportComponent getExportComponent() {
+    return exportComponent;
+  }
+
+  public TraceConfig getTraceConfig() {
+    return traceConfig;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/TracerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/TracerImpl.java
new file mode 100644
index 0000000..48df805
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/TracerImpl.java
@@ -0,0 +1,52 @@
+/*
+ * 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.implcore.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceConfig;
+import javax.annotation.Nullable;
+
+/** Implementation of the {@link Tracer}. */
+public final class TracerImpl extends Tracer {
+  private final SpanBuilderImpl.Options spanBuilderOptions;
+
+  TracerImpl(
+      RandomHandler randomHandler,
+      RecordEventsSpanImpl.StartEndHandler startEndHandler,
+      Clock clock,
+      TraceConfig traceConfig) {
+    spanBuilderOptions =
+        new SpanBuilderImpl.Options(randomHandler, startEndHandler, clock, traceConfig);
+  }
+
+  @Override
+  public SpanBuilder spanBuilderWithExplicitParent(String spanName, @Nullable Span parent) {
+    return SpanBuilderImpl.createWithParent(spanName, parent, spanBuilderOptions);
+  }
+
+  @Override
+  public SpanBuilder spanBuilderWithRemoteParent(
+      String spanName, @Nullable SpanContext remoteParentSpanContext) {
+    return SpanBuilderImpl.createWithRemoteParent(
+        spanName, remoteParentSpanContext, spanBuilderOptions);
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/config/TraceConfigImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/config/TraceConfigImpl.java
new file mode 100644
index 0000000..25f0c61
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/config/TraceConfigImpl.java
@@ -0,0 +1,43 @@
+/*
+ * 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.implcore.trace.config;
+
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.config.TraceParams;
+
+/**
+ * Global configuration of the trace service. This allows users to change configs for the default
+ * sampler, maximum events to be kept, etc.
+ */
+public final class TraceConfigImpl extends TraceConfig {
+  // Reads and writes are atomic for reference variables. Use volatile to ensure that these
+  // operations are visible on other CPUs as well.
+  private volatile TraceParams activeTraceParams = TraceParams.DEFAULT;
+
+  /** Constructs a new {@code TraceConfigImpl}. */
+  public TraceConfigImpl() {}
+
+  @Override
+  public TraceParams getActiveTraceParams() {
+    return activeTraceParams;
+  }
+
+  @Override
+  public void updateActiveTraceParams(TraceParams traceParams) {
+    activeTraceParams = traceParams;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/ExportComponentImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/ExportComponentImpl.java
new file mode 100644
index 0000000..1981738
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/ExportComponentImpl.java
@@ -0,0 +1,93 @@
+/*
+ * 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.implcore.trace.export;
+
+import io.opencensus.common.Duration;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SampledSpanStore;
+
+/** Implementation of the {@link ExportComponent}. */
+public final class ExportComponentImpl extends ExportComponent {
+  private static final int EXPORTER_BUFFER_SIZE = 32;
+  // Enforces that trace export exports data at least once every 5 seconds.
+  private static final Duration EXPORTER_SCHEDULE_DELAY = Duration.create(5, 0);
+
+  private final SpanExporterImpl spanExporter;
+  private final RunningSpanStoreImpl runningSpanStore;
+  private final SampledSpanStoreImpl sampledSpanStore;
+
+  @Override
+  public SpanExporterImpl getSpanExporter() {
+    return spanExporter;
+  }
+
+  @Override
+  public RunningSpanStoreImpl getRunningSpanStore() {
+    return runningSpanStore;
+  }
+
+  @Override
+  public SampledSpanStoreImpl getSampledSpanStore() {
+    return sampledSpanStore;
+  }
+
+  @Override
+  public void shutdown() {
+    sampledSpanStore.shutdown();
+    spanExporter.shutdown();
+  }
+
+  /**
+   * Returns a new {@code ExportComponentImpl} that has valid instances for {@link RunningSpanStore}
+   * and {@link SampledSpanStore}.
+   *
+   * @return a new {@code ExportComponentImpl}.
+   */
+  public static ExportComponentImpl createWithInProcessStores(EventQueue eventQueue) {
+    return new ExportComponentImpl(true, eventQueue);
+  }
+
+  /**
+   * Returns a new {@code ExportComponentImpl} that has {@code null} instances for {@link
+   * RunningSpanStore} and {@link SampledSpanStore}.
+   *
+   * @return a new {@code ExportComponentImpl}.
+   */
+  public static ExportComponentImpl createWithoutInProcessStores(EventQueue eventQueue) {
+    return new ExportComponentImpl(false, eventQueue);
+  }
+
+  /**
+   * Constructs a new {@code ExportComponentImpl}.
+   *
+   * @param supportInProcessStores {@code true} to instantiate {@link RunningSpanStore} and {@link
+   *     SampledSpanStore}.
+   */
+  private ExportComponentImpl(boolean supportInProcessStores, EventQueue eventQueue) {
+    this.spanExporter = SpanExporterImpl.create(EXPORTER_BUFFER_SIZE, EXPORTER_SCHEDULE_DELAY);
+    this.runningSpanStore =
+        supportInProcessStores
+            ? new InProcessRunningSpanStoreImpl()
+            : RunningSpanStoreImpl.getNoopRunningSpanStoreImpl();
+    this.sampledSpanStore =
+        supportInProcessStores
+            ? new InProcessSampledSpanStoreImpl(eventQueue)
+            : SampledSpanStoreImpl.getNoopSampledSpanStoreImpl();
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImpl.java
new file mode 100644
index 0000000..f7aeac7
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImpl.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018, 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.implcore.trace.export;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.internal.ConcurrentIntrusiveList;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SpanData;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** In-process implementation of the {@link RunningSpanStore}. */
+@ThreadSafe
+public final class InProcessRunningSpanStoreImpl extends RunningSpanStoreImpl {
+  private final ConcurrentIntrusiveList<RecordEventsSpanImpl> runningSpans;
+
+  public InProcessRunningSpanStoreImpl() {
+    runningSpans = new ConcurrentIntrusiveList<RecordEventsSpanImpl>();
+  }
+
+  @Override
+  public void onStart(RecordEventsSpanImpl span) {
+    runningSpans.addElement(span);
+  }
+
+  @Override
+  public void onEnd(RecordEventsSpanImpl span) {
+    runningSpans.removeElement(span);
+  }
+
+  @Override
+  public Summary getSummary() {
+    Collection<RecordEventsSpanImpl> allRunningSpans = runningSpans.getAll();
+    Map<String, Integer> numSpansPerName = new HashMap<String, Integer>();
+    for (RecordEventsSpanImpl span : allRunningSpans) {
+      Integer prevValue = numSpansPerName.get(span.getName());
+      numSpansPerName.put(span.getName(), prevValue != null ? prevValue + 1 : 1);
+    }
+    Map<String, PerSpanNameSummary> perSpanNameSummary = new HashMap<String, PerSpanNameSummary>();
+    for (Map.Entry<String, Integer> it : numSpansPerName.entrySet()) {
+      perSpanNameSummary.put(it.getKey(), PerSpanNameSummary.create(it.getValue()));
+    }
+    Summary summary = Summary.create(perSpanNameSummary);
+    return summary;
+  }
+
+  @Override
+  public Collection<SpanData> getRunningSpans(Filter filter) {
+    Collection<RecordEventsSpanImpl> allRunningSpans = runningSpans.getAll();
+    int maxSpansToReturn =
+        filter.getMaxSpansToReturn() == 0 ? allRunningSpans.size() : filter.getMaxSpansToReturn();
+    List<SpanData> ret = new ArrayList<SpanData>(maxSpansToReturn);
+    for (RecordEventsSpanImpl span : allRunningSpans) {
+      if (ret.size() == maxSpansToReturn) {
+        break;
+      }
+      if (span.getName().equals(filter.getSpanName())) {
+        ret.add(span.toSpanData());
+      }
+    }
+    return ret;
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImpl.java
new file mode 100644
index 0000000..0d8e493
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImpl.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2018, 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.implcore.trace.export;
+
+import com.google.common.collect.EvictingQueue;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.export.SampledSpanStore;
+import io.opencensus.trace.export.SpanData;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** In-process implementation of the {@link SampledSpanStore}. */
+@ThreadSafe
+public final class InProcessSampledSpanStoreImpl extends SampledSpanStoreImpl {
+  private static final int NUM_SAMPLES_PER_LATENCY_BUCKET = 10;
+  private static final int NUM_SAMPLES_PER_ERROR_BUCKET = 5;
+  private static final long TIME_BETWEEN_SAMPLES = TimeUnit.SECONDS.toNanos(1);
+  private static final int NUM_LATENCY_BUCKETS = LatencyBucketBoundaries.values().length;
+  // The total number of canonical codes - 1 (the OK code).
+  private static final int NUM_ERROR_BUCKETS = CanonicalCode.values().length - 1;
+  private static final int MAX_PER_SPAN_NAME_SAMPLES =
+      NUM_SAMPLES_PER_LATENCY_BUCKET * NUM_LATENCY_BUCKETS
+          + NUM_SAMPLES_PER_ERROR_BUCKET * NUM_ERROR_BUCKETS;
+
+  // Used to stream the register/unregister events to the implementation to avoid lock contention
+  // between the main threads and the worker thread.
+  private final EventQueue eventQueue;
+
+  @GuardedBy("samples")
+  private final Map<String, PerSpanNameSamples> samples;
+
+  private static final class Bucket {
+
+    private final EvictingQueue<RecordEventsSpanImpl> sampledSpansQueue;
+    private final EvictingQueue<RecordEventsSpanImpl> notSampledSpansQueue;
+    private long lastSampledNanoTime;
+    private long lastNotSampledNanoTime;
+
+    private Bucket(int numSamples) {
+      sampledSpansQueue = EvictingQueue.create(numSamples);
+      notSampledSpansQueue = EvictingQueue.create(numSamples);
+    }
+
+    private void considerForSampling(RecordEventsSpanImpl span) {
+      long spanEndNanoTime = span.getEndNanoTime();
+      if (span.getContext().getTraceOptions().isSampled()) {
+        // Need to compare by doing the subtraction all the time because in case of an overflow,
+        // this may never sample again (at least for the next ~200 years). No real chance to
+        // overflow two times because that means the process runs for ~200 years.
+        if (spanEndNanoTime - lastSampledNanoTime > TIME_BETWEEN_SAMPLES) {
+          sampledSpansQueue.add(span);
+          lastSampledNanoTime = spanEndNanoTime;
+        }
+      } else {
+        // Need to compare by doing the subtraction all the time because in case of an overflow,
+        // this may never sample again (at least for the next ~200 years). No real chance to
+        // overflow two times because that means the process runs for ~200 years.
+        if (spanEndNanoTime - lastNotSampledNanoTime > TIME_BETWEEN_SAMPLES) {
+          notSampledSpansQueue.add(span);
+          lastNotSampledNanoTime = spanEndNanoTime;
+        }
+      }
+    }
+
+    private void getSamples(int maxSpansToReturn, List<RecordEventsSpanImpl> output) {
+      getSamples(maxSpansToReturn, output, sampledSpansQueue);
+      getSamples(maxSpansToReturn, output, notSampledSpansQueue);
+    }
+
+    private static void getSamples(
+        int maxSpansToReturn,
+        List<RecordEventsSpanImpl> output,
+        EvictingQueue<RecordEventsSpanImpl> queue) {
+      for (RecordEventsSpanImpl span : queue) {
+        if (output.size() >= maxSpansToReturn) {
+          break;
+        }
+        output.add(span);
+      }
+    }
+
+    private void getSamplesFilteredByLatency(
+        long latencyLowerNs,
+        long latencyUpperNs,
+        int maxSpansToReturn,
+        List<RecordEventsSpanImpl> output) {
+      getSamplesFilteredByLatency(
+          latencyLowerNs, latencyUpperNs, maxSpansToReturn, output, sampledSpansQueue);
+      getSamplesFilteredByLatency(
+          latencyLowerNs, latencyUpperNs, maxSpansToReturn, output, notSampledSpansQueue);
+    }
+
+    private static void getSamplesFilteredByLatency(
+        long latencyLowerNs,
+        long latencyUpperNs,
+        int maxSpansToReturn,
+        List<RecordEventsSpanImpl> output,
+        EvictingQueue<RecordEventsSpanImpl> queue) {
+      for (RecordEventsSpanImpl span : queue) {
+        if (output.size() >= maxSpansToReturn) {
+          break;
+        }
+        long spanLatencyNs = span.getLatencyNs();
+        if (spanLatencyNs >= latencyLowerNs && spanLatencyNs < latencyUpperNs) {
+          output.add(span);
+        }
+      }
+    }
+
+    private int getNumSamples() {
+      return sampledSpansQueue.size() + notSampledSpansQueue.size();
+    }
+  }
+
+  /**
+   * Keeps samples for a given span name. Samples for all the latency buckets and for all canonical
+   * codes other than OK.
+   */
+  private static final class PerSpanNameSamples {
+
+    private final Bucket[] latencyBuckets;
+    private final Bucket[] errorBuckets;
+
+    private PerSpanNameSamples() {
+      latencyBuckets = new Bucket[NUM_LATENCY_BUCKETS];
+      for (int i = 0; i < NUM_LATENCY_BUCKETS; i++) {
+        latencyBuckets[i] = new Bucket(NUM_SAMPLES_PER_LATENCY_BUCKET);
+      }
+      errorBuckets = new Bucket[NUM_ERROR_BUCKETS];
+      for (int i = 0; i < NUM_ERROR_BUCKETS; i++) {
+        errorBuckets[i] = new Bucket(NUM_SAMPLES_PER_ERROR_BUCKET);
+      }
+    }
+
+    @Nullable
+    private Bucket getLatencyBucket(long latencyNs) {
+      for (int i = 0; i < NUM_LATENCY_BUCKETS; i++) {
+        LatencyBucketBoundaries boundaries = LatencyBucketBoundaries.values()[i];
+        if (latencyNs >= boundaries.getLatencyLowerNs()
+            && latencyNs < boundaries.getLatencyUpperNs()) {
+          return latencyBuckets[i];
+        }
+      }
+      // latencyNs is negative or Long.MAX_VALUE, so this Span can be ignored. This cannot happen
+      // in real production because System#nanoTime is monotonic.
+      return null;
+    }
+
+    private Bucket getErrorBucket(CanonicalCode code) {
+      return errorBuckets[code.value() - 1];
+    }
+
+    private void considerForSampling(RecordEventsSpanImpl span) {
+      Status status = span.getStatus();
+      // Null status means running Span, this should not happen in production, but the library
+      // should not crash because of this.
+      if (status != null) {
+        Bucket bucket =
+            status.isOk()
+                ? getLatencyBucket(span.getLatencyNs())
+                : getErrorBucket(status.getCanonicalCode());
+        // If unable to find the bucket, ignore this Span.
+        if (bucket != null) {
+          bucket.considerForSampling(span);
+        }
+      }
+    }
+
+    private Map<LatencyBucketBoundaries, Integer> getNumbersOfLatencySampledSpans() {
+      Map<LatencyBucketBoundaries, Integer> latencyBucketSummaries =
+          new EnumMap<LatencyBucketBoundaries, Integer>(LatencyBucketBoundaries.class);
+      for (int i = 0; i < NUM_LATENCY_BUCKETS; i++) {
+        latencyBucketSummaries.put(
+            LatencyBucketBoundaries.values()[i], latencyBuckets[i].getNumSamples());
+      }
+      return latencyBucketSummaries;
+    }
+
+    private Map<CanonicalCode, Integer> getNumbersOfErrorSampledSpans() {
+      Map<CanonicalCode, Integer> errorBucketSummaries =
+          new EnumMap<CanonicalCode, Integer>(CanonicalCode.class);
+      for (int i = 0; i < NUM_ERROR_BUCKETS; i++) {
+        errorBucketSummaries.put(CanonicalCode.values()[i + 1], errorBuckets[i].getNumSamples());
+      }
+      return errorBucketSummaries;
+    }
+
+    private List<RecordEventsSpanImpl> getErrorSamples(
+        @Nullable CanonicalCode code, int maxSpansToReturn) {
+      ArrayList<RecordEventsSpanImpl> output =
+          new ArrayList<RecordEventsSpanImpl>(maxSpansToReturn);
+      if (code != null) {
+        getErrorBucket(code).getSamples(maxSpansToReturn, output);
+      } else {
+        for (int i = 0; i < NUM_ERROR_BUCKETS; i++) {
+          errorBuckets[i].getSamples(maxSpansToReturn, output);
+        }
+      }
+      return output;
+    }
+
+    private List<RecordEventsSpanImpl> getLatencySamples(
+        long latencyLowerNs, long latencyUpperNs, int maxSpansToReturn) {
+      ArrayList<RecordEventsSpanImpl> output =
+          new ArrayList<RecordEventsSpanImpl>(maxSpansToReturn);
+      for (int i = 0; i < NUM_LATENCY_BUCKETS; i++) {
+        LatencyBucketBoundaries boundaries = LatencyBucketBoundaries.values()[i];
+        if (latencyUpperNs >= boundaries.getLatencyLowerNs()
+            && latencyLowerNs < boundaries.getLatencyUpperNs()) {
+          latencyBuckets[i].getSamplesFilteredByLatency(
+              latencyLowerNs, latencyUpperNs, maxSpansToReturn, output);
+        }
+      }
+      return output;
+    }
+  }
+
+  /** Constructs a new {@code InProcessSampledSpanStoreImpl}. */
+  InProcessSampledSpanStoreImpl(EventQueue eventQueue) {
+    samples = new HashMap<String, PerSpanNameSamples>();
+    this.eventQueue = eventQueue;
+  }
+
+  @Override
+  public Summary getSummary() {
+    Map<String, PerSpanNameSummary> ret = new HashMap<String, PerSpanNameSummary>();
+    synchronized (samples) {
+      for (Map.Entry<String, PerSpanNameSamples> it : samples.entrySet()) {
+        ret.put(
+            it.getKey(),
+            PerSpanNameSummary.create(
+                it.getValue().getNumbersOfLatencySampledSpans(),
+                it.getValue().getNumbersOfErrorSampledSpans()));
+      }
+    }
+    return Summary.create(ret);
+  }
+
+  @Override
+  public void considerForSampling(RecordEventsSpanImpl span) {
+    synchronized (samples) {
+      String spanName = span.getName();
+      if (span.getSampleToLocalSpanStore() && !samples.containsKey(spanName)) {
+        samples.put(spanName, new PerSpanNameSamples());
+      }
+      PerSpanNameSamples perSpanNameSamples = samples.get(spanName);
+      if (perSpanNameSamples != null) {
+        perSpanNameSamples.considerForSampling(span);
+      }
+    }
+  }
+
+  @Override
+  public void registerSpanNamesForCollection(Collection<String> spanNames) {
+    eventQueue.enqueue(new RegisterSpanNameEvent(this, spanNames));
+  }
+
+  @Override
+  protected void shutdown() {
+    eventQueue.shutdown();
+  }
+
+  private void internaltRegisterSpanNamesForCollection(Collection<String> spanNames) {
+    synchronized (samples) {
+      for (String spanName : spanNames) {
+        if (!samples.containsKey(spanName)) {
+          samples.put(spanName, new PerSpanNameSamples());
+        }
+      }
+    }
+  }
+
+  private static final class RegisterSpanNameEvent implements EventQueue.Entry {
+    private final InProcessSampledSpanStoreImpl sampledSpanStore;
+    private final Collection<String> spanNames;
+
+    private RegisterSpanNameEvent(
+        InProcessSampledSpanStoreImpl sampledSpanStore, Collection<String> spanNames) {
+      this.sampledSpanStore = sampledSpanStore;
+      this.spanNames = new ArrayList<String>(spanNames);
+    }
+
+    @Override
+    public void process() {
+      sampledSpanStore.internaltRegisterSpanNamesForCollection(spanNames);
+    }
+  }
+
+  @Override
+  public void unregisterSpanNamesForCollection(Collection<String> spanNames) {
+    eventQueue.enqueue(new UnregisterSpanNameEvent(this, spanNames));
+  }
+
+  private void internalUnregisterSpanNamesForCollection(Collection<String> spanNames) {
+    synchronized (samples) {
+      samples.keySet().removeAll(spanNames);
+    }
+  }
+
+  private static final class UnregisterSpanNameEvent implements EventQueue.Entry {
+    private final InProcessSampledSpanStoreImpl sampledSpanStore;
+    private final Collection<String> spanNames;
+
+    private UnregisterSpanNameEvent(
+        InProcessSampledSpanStoreImpl sampledSpanStore, Collection<String> spanNames) {
+      this.sampledSpanStore = sampledSpanStore;
+      this.spanNames = new ArrayList<String>(spanNames);
+    }
+
+    @Override
+    public void process() {
+      sampledSpanStore.internalUnregisterSpanNamesForCollection(spanNames);
+    }
+  }
+
+  @Override
+  public Set<String> getRegisteredSpanNamesForCollection() {
+    synchronized (samples) {
+      return Collections.unmodifiableSet(new HashSet<String>(samples.keySet()));
+    }
+  }
+
+  @Override
+  public Collection<SpanData> getErrorSampledSpans(ErrorFilter filter) {
+    int numSpansToReturn =
+        filter.getMaxSpansToReturn() == 0
+            ? MAX_PER_SPAN_NAME_SAMPLES
+            : filter.getMaxSpansToReturn();
+    List<RecordEventsSpanImpl> spans = Collections.emptyList();
+    // Try to not keep the lock to much, do the RecordEventsSpanImpl -> SpanData conversion outside
+    // the lock.
+    synchronized (samples) {
+      PerSpanNameSamples perSpanNameSamples = samples.get(filter.getSpanName());
+      if (perSpanNameSamples != null) {
+        spans = perSpanNameSamples.getErrorSamples(filter.getCanonicalCode(), numSpansToReturn);
+      }
+    }
+    List<SpanData> ret = new ArrayList<SpanData>(spans.size());
+    for (RecordEventsSpanImpl span : spans) {
+      ret.add(span.toSpanData());
+    }
+    return Collections.unmodifiableList(ret);
+  }
+
+  @Override
+  public Collection<SpanData> getLatencySampledSpans(LatencyFilter filter) {
+    int numSpansToReturn =
+        filter.getMaxSpansToReturn() == 0
+            ? MAX_PER_SPAN_NAME_SAMPLES
+            : filter.getMaxSpansToReturn();
+    List<RecordEventsSpanImpl> spans = Collections.emptyList();
+    // Try to not keep the lock to much, do the RecordEventsSpanImpl -> SpanData conversion outside
+    // the lock.
+    synchronized (samples) {
+      PerSpanNameSamples perSpanNameSamples = samples.get(filter.getSpanName());
+      if (perSpanNameSamples != null) {
+        spans =
+            perSpanNameSamples.getLatencySamples(
+                filter.getLatencyLowerNs(), filter.getLatencyUpperNs(), numSpansToReturn);
+      }
+    }
+    List<SpanData> ret = new ArrayList<SpanData>(spans.size());
+    for (RecordEventsSpanImpl span : spans) {
+      ret.add(span.toSpanData());
+    }
+    return Collections.unmodifiableList(ret);
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/RunningSpanStoreImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/RunningSpanStoreImpl.java
new file mode 100644
index 0000000..962f5b0
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/RunningSpanStoreImpl.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018, 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.implcore.trace.export;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SpanData;
+import java.util.Collection;
+import java.util.Collections;
+
+/** Abstract implementation of the {@link RunningSpanStore}. */
+public abstract class RunningSpanStoreImpl extends RunningSpanStore {
+
+  private static final RunningSpanStoreImpl NOOP_RUNNING_SPAN_STORE_IMPL =
+      new NoopRunningSpanStoreImpl();
+
+  /** Returns the no-op implementation of the {@link RunningSpanStoreImpl}. */
+  static RunningSpanStoreImpl getNoopRunningSpanStoreImpl() {
+    return NOOP_RUNNING_SPAN_STORE_IMPL;
+  }
+
+  /**
+   * Adds the {@code Span} into the running spans list when the {@code Span} starts.
+   *
+   * @param span the {@code Span} that started.
+   */
+  public abstract void onStart(RecordEventsSpanImpl span);
+
+  /**
+   * Removes the {@code Span} from the running spans list when the {@code Span} ends.
+   *
+   * @param span the {@code Span} that ended.
+   */
+  public abstract void onEnd(RecordEventsSpanImpl span);
+
+  private static final class NoopRunningSpanStoreImpl extends RunningSpanStoreImpl {
+
+    private static final Summary EMPTY_SUMMARY =
+        RunningSpanStore.Summary.create(Collections.<String, PerSpanNameSummary>emptyMap());
+
+    @Override
+    public void onStart(RecordEventsSpanImpl span) {}
+
+    @Override
+    public void onEnd(RecordEventsSpanImpl span) {}
+
+    @Override
+    public Summary getSummary() {
+      return EMPTY_SUMMARY;
+    }
+
+    @Override
+    public Collection<SpanData> getRunningSpans(Filter filter) {
+      return Collections.<SpanData>emptyList();
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/SampledSpanStoreImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/SampledSpanStoreImpl.java
new file mode 100644
index 0000000..e67c2f8
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/SampledSpanStoreImpl.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018, 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.implcore.trace.export;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.trace.export.SampledSpanStore;
+import io.opencensus.trace.export.SpanData;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/** Abstract implementation of the {@link SampledSpanStore}. */
+public abstract class SampledSpanStoreImpl extends SampledSpanStore {
+  private static final SampledSpanStoreImpl NOOP_SAMPLED_SPAN_STORE_IMPL =
+      new NoopSampledSpanStoreImpl();
+
+  /** Returns the new no-op implmentation of {@link SampledSpanStoreImpl}. */
+  public static SampledSpanStoreImpl getNoopSampledSpanStoreImpl() {
+    return NOOP_SAMPLED_SPAN_STORE_IMPL;
+  }
+
+  /**
+   * Considers to save the given spans to the stored samples. This must be called at the end of each
+   * Span with the option RECORD_EVENTS.
+   *
+   * @param span the span to be consider for storing into the store buckets.
+   */
+  public abstract void considerForSampling(RecordEventsSpanImpl span);
+
+  protected void shutdown() {}
+
+  private static final class NoopSampledSpanStoreImpl extends SampledSpanStoreImpl {
+    private static final Summary EMPTY_SUMMARY =
+        Summary.create(Collections.<String, PerSpanNameSummary>emptyMap());
+    private static final Set<String> EMPTY_REGISTERED_SPAN_NAMES = Collections.<String>emptySet();
+    private static final Collection<SpanData> EMPTY_SPANDATA = Collections.<SpanData>emptySet();
+
+    @Override
+    public Summary getSummary() {
+      return EMPTY_SUMMARY;
+    }
+
+    @Override
+    public void considerForSampling(RecordEventsSpanImpl span) {}
+
+    @Override
+    public void registerSpanNamesForCollection(Collection<String> spanNames) {}
+
+    @Override
+    public void unregisterSpanNamesForCollection(Collection<String> spanNames) {}
+
+    @Override
+    public Set<String> getRegisteredSpanNamesForCollection() {
+      return EMPTY_REGISTERED_SPAN_NAMES;
+    }
+
+    @Override
+    public Collection<SpanData> getErrorSampledSpans(ErrorFilter filter) {
+      return EMPTY_SPANDATA;
+    }
+
+    @Override
+    public Collection<SpanData> getLatencySampledSpans(LatencyFilter filter) {
+      return EMPTY_SPANDATA;
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/SpanExporterImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/SpanExporterImpl.java
new file mode 100644
index 0000000..51a7b05
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/SpanExporterImpl.java
@@ -0,0 +1,214 @@
+/*
+ * 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.implcore.trace.export;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.Duration;
+import io.opencensus.implcore.internal.DaemonThreadFactory;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.GuardedBy;
+
+/** Implementation of the {@link SpanExporter}. */
+public final class SpanExporterImpl extends SpanExporter {
+  private static final Logger logger = Logger.getLogger(ExportComponent.class.getName());
+
+  private final Worker worker;
+  private final Thread workerThread;
+
+  /**
+   * Constructs a {@code SpanExporterImpl} that exports the {@link SpanData} asynchronously.
+   *
+   * <p>Starts a separate thread that wakes up every {@code scheduleDelay} and exports any available
+   * spans data. If the number of buffered SpanData objects is greater than {@code bufferSize} then
+   * the thread wakes up sooner.
+   *
+   * @param bufferSize the size of the buffered span data.
+   * @param scheduleDelay the maximum delay.
+   */
+  static SpanExporterImpl create(int bufferSize, Duration scheduleDelay) {
+    // TODO(bdrutu): Consider to add a shutdown hook to not avoid dropping data.
+    Worker worker = new Worker(bufferSize, scheduleDelay);
+    return new SpanExporterImpl(worker);
+  }
+
+  /**
+   * Adds a Span to the exporting service.
+   *
+   * @param span the {@code Span} to be added.
+   */
+  public void addSpan(RecordEventsSpanImpl span) {
+    worker.addSpan(span);
+  }
+
+  @Override
+  public void registerHandler(String name, Handler handler) {
+    worker.registerHandler(name, handler);
+  }
+
+  @Override
+  public void unregisterHandler(String name) {
+    worker.unregisterHandler(name);
+  }
+
+  void flush() {
+    worker.flush();
+  }
+
+  void shutdown() {
+    flush();
+    workerThread.interrupt();
+  }
+
+  private SpanExporterImpl(Worker worker) {
+    this.workerThread =
+        new DaemonThreadFactory("ExportComponent.ServiceExporterThread").newThread(worker);
+    this.workerThread.start();
+    this.worker = worker;
+  }
+
+  @VisibleForTesting
+  Thread getServiceExporterThread() {
+    return workerThread;
+  }
+
+  // Worker in a thread that batches multiple span data and calls the registered services to export
+  // that data.
+  //
+  // The map of registered handlers is implemented using ConcurrentHashMap ensuring full
+  // concurrency of retrievals and adjustable expected concurrency for updates. Retrievals
+  // reflect the results of the most recently completed update operations held upon their onset.
+  //
+  // The list of batched data is protected by an explicit monitor object which ensures full
+  // concurrency.
+  private static final class Worker implements Runnable {
+    private final Object monitor = new Object();
+
+    @GuardedBy("monitor")
+    private final List<RecordEventsSpanImpl> spans;
+
+    private final Map<String, Handler> serviceHandlers = new ConcurrentHashMap<String, Handler>();
+    private final int bufferSize;
+    private final long scheduleDelayMillis;
+
+    // See SpanExporterImpl#addSpan.
+    private void addSpan(RecordEventsSpanImpl span) {
+      synchronized (monitor) {
+        this.spans.add(span);
+        if (spans.size() > bufferSize) {
+          monitor.notifyAll();
+        }
+      }
+    }
+
+    // See SpanExporter#registerHandler.
+    private void registerHandler(String name, Handler serviceHandler) {
+      serviceHandlers.put(name, serviceHandler);
+    }
+
+    // See SpanExporter#unregisterHandler.
+    private void unregisterHandler(String name) {
+      serviceHandlers.remove(name);
+    }
+
+    // Exports the list of SpanData to all the ServiceHandlers.
+    private void onBatchExport(List<SpanData> spanDataList) {
+      // From the java documentation of the ConcurrentHashMap#entrySet():
+      // The view's iterator is a "weakly consistent" iterator that will never throw
+      // ConcurrentModificationException, and guarantees to traverse elements as they existed
+      // upon construction of the iterator, and may (but is not guaranteed to) reflect any
+      // modifications subsequent to construction.
+      for (Map.Entry<String, Handler> it : serviceHandlers.entrySet()) {
+        // In case of any exception thrown by the service handlers continue to run.
+        try {
+          it.getValue().export(spanDataList);
+        } catch (Throwable e) {
+          logger.log(Level.WARNING, "Exception thrown by the service export " + it.getKey(), e);
+        }
+      }
+    }
+
+    private Worker(int bufferSize, Duration scheduleDelay) {
+      spans = new ArrayList<RecordEventsSpanImpl>(bufferSize);
+      this.bufferSize = bufferSize;
+      this.scheduleDelayMillis = scheduleDelay.toMillis();
+    }
+
+    // Returns an unmodifiable list of all buffered spans data to ensure that any registered
+    // service handler cannot modify the list.
+    private static List<SpanData> fromSpanImplToSpanData(List<RecordEventsSpanImpl> spans) {
+      List<SpanData> spanDatas = new ArrayList<SpanData>(spans.size());
+      for (RecordEventsSpanImpl span : spans) {
+        spanDatas.add(span.toSpanData());
+      }
+      return Collections.unmodifiableList(spanDatas);
+    }
+
+    @Override
+    public void run() {
+      while (true) {
+        // Copy all the batched spans in a separate list to release the monitor lock asap to
+        // avoid blocking the producer thread.
+        List<RecordEventsSpanImpl> spansCopy;
+        synchronized (monitor) {
+          if (spans.size() < bufferSize) {
+            do {
+              // In the case of a spurious wakeup we export only if we have at least one span in
+              // the batch. It is acceptable because batching is a best effort mechanism here.
+              try {
+                monitor.wait(scheduleDelayMillis);
+              } catch (InterruptedException ie) {
+                // Preserve the interruption status as per guidance and stop doing any work.
+                Thread.currentThread().interrupt();
+                return;
+              }
+            } while (spans.isEmpty());
+          }
+          spansCopy = new ArrayList<RecordEventsSpanImpl>(spans);
+          spans.clear();
+        }
+        // Execute the batch export outside the synchronized to not block all producers.
+        final List<SpanData> spanDataList = fromSpanImplToSpanData(spansCopy);
+        if (!spanDataList.isEmpty()) {
+          onBatchExport(spanDataList);
+        }
+      }
+    }
+
+    void flush() {
+      List<RecordEventsSpanImpl> spansCopy;
+      synchronized (monitor) {
+        spansCopy = new ArrayList<RecordEventsSpanImpl>(spans);
+        spans.clear();
+      }
+
+      final List<SpanData> spanDataList = fromSpanImplToSpanData(spansCopy);
+      if (!spanDataList.isEmpty()) {
+        onBatchExport(spanDataList);
+      }
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveList.java b/impl_core/src/main/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveList.java
new file mode 100644
index 0000000..22d8e41
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveList.java
@@ -0,0 +1,181 @@
+/*
+ * 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.implcore.trace.internal;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import io.opencensus.implcore.internal.CheckerFrameworkUtils;
+import io.opencensus.implcore.trace.internal.ConcurrentIntrusiveList.Element;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An {@code ConcurrentIntrusiveList<T>} is a doubly-linked list where the link pointers are
+ * embedded in the elements. This makes insertion and removal into a known position constant time.
+ *
+ * <p>Elements must derive from the {@code Element<T extends Element<T>>} interface:
+ *
+ * <pre><code>
+ * class MyClass implements {@code Element<MyClass>} {
+ *   private MyClass next = null;
+ *   private MyClass prev = null;
+ *
+ *  {@literal @}Override
+ *   MyClass getNext() {
+ *     return next;
+ *   }
+ *
+ *  {@literal @}Override
+ *   void setNext(MyClass element) {
+ *     next = element;
+ *   }
+ *
+ *  {@literal @}Override
+ *   MyClass getPrev() {
+ *     return prev;
+ *   }
+ *
+ *  {@literal @}Override
+ *   void setPrev(MyClass element) {
+ *     prev = element;
+ *   }
+ * }
+ * </code></pre>
+ */
+@ThreadSafe
+public final class ConcurrentIntrusiveList<T extends Element<T>> {
+  private int size = 0;
+  @Nullable private T head = null;
+
+  public ConcurrentIntrusiveList() {}
+
+  /**
+   * Adds the given {@code element} to the list.
+   *
+   * @param element the element to add.
+   * @throws IllegalArgumentException if the element is already in a list.
+   */
+  public synchronized void addElement(T element) {
+    checkArgument(
+        element.getNext() == null && element.getPrev() == null && element != head,
+        "Element already in a list.");
+    size++;
+    if (head == null) {
+      head = element;
+    } else {
+      head.setPrev(element);
+      element.setNext(head);
+      head = element;
+    }
+  }
+
+  /**
+   * Removes the given {@code element} from the list.
+   *
+   * @param element the element to remove.
+   * @throws IllegalArgumentException if the element is not in the list.
+   */
+  public synchronized void removeElement(T element) {
+    checkArgument(
+        element.getNext() != null || element.getPrev() != null || element == head,
+        "Element not in the list.");
+    size--;
+    if (element.getPrev() == null) {
+      // This is the first element
+      head = element.getNext();
+      if (head != null) {
+        // If more than one element in the list.
+        head.setPrev(null);
+        element.setNext(null);
+      }
+    } else if (element.getNext() == null) {
+      // This is the last element, and there is at least another element because
+      // element.getPrev() != null.
+      CheckerFrameworkUtils.castNonNull(element.getPrev()).setNext(null);
+      element.setPrev(null);
+    } else {
+      CheckerFrameworkUtils.castNonNull(element.getPrev()).setNext(element.getNext());
+      CheckerFrameworkUtils.castNonNull(element.getNext()).setPrev(element.getPrev());
+      element.setNext(null);
+      element.setPrev(null);
+    }
+  }
+
+  /**
+   * Returns the number of elements in this list.
+   *
+   * @return the number of elements in this list.
+   */
+  public synchronized int size() {
+    return size;
+  }
+
+  /**
+   * Returns all the elements from this list.
+   *
+   * @return all the elements from this list.
+   */
+  public synchronized Collection<T> getAll() {
+    List<T> all = new ArrayList<T>(size);
+    for (T e = head; e != null; e = e.getNext()) {
+      all.add(e);
+    }
+    return all;
+  }
+
+  /**
+   * This is an interface that must be implemented by any element that uses {@link
+   * ConcurrentIntrusiveList}.
+   *
+   * @param <T> the element that will be used for the list.
+   */
+  public interface Element<T extends Element<T>> {
+
+    /**
+     * Returns a reference to the next element in the list.
+     *
+     * @return a reference to the next element in the list.
+     */
+    @Nullable
+    T getNext();
+
+    /**
+     * Sets the reference to the next element in the list.
+     *
+     * @param element the reference to the next element in the list.
+     */
+    void setNext(@Nullable T element);
+
+    /**
+     * Returns a reference to the previous element in the list.
+     *
+     * @return a reference to the previous element in the list.
+     */
+    @Nullable
+    T getPrev();
+
+    /**
+     * Sets the reference to the previous element in the list.
+     *
+     * @param element the reference to the previous element in the list.
+     */
+    void setPrev(@Nullable T element);
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/internal/RandomHandler.java b/impl_core/src/main/java/io/opencensus/implcore/trace/internal/RandomHandler.java
new file mode 100644
index 0000000..70be5a9
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/internal/RandomHandler.java
@@ -0,0 +1,50 @@
+/*
+ * 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.implcore.trace.internal;
+
+import java.security.SecureRandom;
+import java.util.Random;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Abstract class to access the current {@link Random}.
+ *
+ * <p>Implementation can have a per thread instance or a single global instance.
+ */
+@ThreadSafe
+public abstract class RandomHandler {
+  /**
+   * Returns the current {@link Random}.
+   *
+   * @return the current {@code Random}.
+   */
+  public abstract Random current();
+
+  /** Implementation of the {@link RandomHandler} using {@link SecureRandom}. */
+  @ThreadSafe
+  public static final class SecureRandomHandler extends RandomHandler {
+    private final Random random = new SecureRandom();
+
+    /** Constructs a new {@link SecureRandomHandler}. */
+    public SecureRandomHandler() {}
+
+    @Override
+    public Random current() {
+      return random;
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/B3Format.java b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/B3Format.java
new file mode 100644
index 0000000..d928d93
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/B3Format.java
@@ -0,0 +1,113 @@
+/*
+ * 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.implcore.trace.propagation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.NonNull;
+*/
+
+/**
+ * Implementation of the B3 propagation protocol. See <a
+ * href=https://github.com/openzipkin/b3-propagation>b3-propagation</a>.
+ */
+final class B3Format extends TextFormat {
+  private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+  @VisibleForTesting static final String X_B3_TRACE_ID = "X-B3-TraceId";
+  @VisibleForTesting static final String X_B3_SPAN_ID = "X-B3-SpanId";
+  @VisibleForTesting static final String X_B3_PARENT_SPAN_ID = "X-B3-ParentSpanId";
+  @VisibleForTesting static final String X_B3_SAMPLED = "X-B3-Sampled";
+  @VisibleForTesting static final String X_B3_FLAGS = "X-B3-Flags";
+  private static final List<String> FIELDS =
+      Collections.unmodifiableList(
+          Arrays.asList(
+              X_B3_TRACE_ID, X_B3_SPAN_ID, X_B3_PARENT_SPAN_ID, X_B3_SAMPLED, X_B3_FLAGS));
+
+  // Used as the upper TraceId.SIZE hex characters of the traceID. B3-propagation used to send
+  // TraceId.SIZE hex characters (8-bytes traceId) in the past.
+  private static final String UPPER_TRACE_ID = "0000000000000000";
+  // Sampled value via the X_B3_SAMPLED header.
+  private static final String SAMPLED_VALUE = "1";
+  // "Debug" sampled value.
+  private static final String FLAGS_VALUE = "1";
+
+  @Override
+  public List<String> fields() {
+    return FIELDS;
+  }
+
+  @Override
+  public <C /*>>> extends @NonNull Object*/> void inject(
+      SpanContext spanContext, C carrier, Setter<C> setter) {
+    checkNotNull(spanContext, "spanContext");
+    checkNotNull(setter, "setter");
+    checkNotNull(carrier, "carrier");
+    setter.put(carrier, X_B3_TRACE_ID, spanContext.getTraceId().toLowerBase16());
+    setter.put(carrier, X_B3_SPAN_ID, spanContext.getSpanId().toLowerBase16());
+    if (spanContext.getTraceOptions().isSampled()) {
+      setter.put(carrier, X_B3_SAMPLED, SAMPLED_VALUE);
+    }
+  }
+
+  @Override
+  public <C /*>>> extends @NonNull Object*/> SpanContext extract(C carrier, Getter<C> getter)
+      throws SpanContextParseException {
+    checkNotNull(carrier, "carrier");
+    checkNotNull(getter, "getter");
+    try {
+      TraceId traceId;
+      String traceIdStr = getter.get(carrier, X_B3_TRACE_ID);
+      if (traceIdStr != null) {
+        if (traceIdStr.length() == TraceId.SIZE) {
+          // This is an 8-byte traceID.
+          traceIdStr = UPPER_TRACE_ID + traceIdStr;
+        }
+        traceId = TraceId.fromLowerBase16(traceIdStr);
+      } else {
+        throw new SpanContextParseException("Missing X_B3_TRACE_ID.");
+      }
+      SpanId spanId;
+      String spanIdStr = getter.get(carrier, X_B3_SPAN_ID);
+      if (spanIdStr != null) {
+        spanId = SpanId.fromLowerBase16(spanIdStr);
+      } else {
+        throw new SpanContextParseException("Missing X_B3_SPAN_ID.");
+      }
+      TraceOptions traceOptions = TraceOptions.DEFAULT;
+      if (SAMPLED_VALUE.equals(getter.get(carrier, X_B3_SAMPLED))
+          || FLAGS_VALUE.equals(getter.get(carrier, X_B3_FLAGS))) {
+        traceOptions = TraceOptions.builder().setIsSampled(true).build();
+      }
+      return SpanContext.create(traceId, spanId, traceOptions, TRACESTATE_DEFAULT);
+    } catch (IllegalArgumentException e) {
+      throw new SpanContextParseException("Invalid input.", e);
+    }
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/BinaryFormatImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/BinaryFormatImpl.java
new file mode 100644
index 0000000..233fbd3
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/BinaryFormatImpl.java
@@ -0,0 +1,148 @@
+/*
+ * 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.implcore.trace.propagation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.BinaryFormat;
+import io.opencensus.trace.propagation.SpanContextParseException;
+
+/**
+ * Implementation of the {@link BinaryFormat}.
+ *
+ * <p>BinaryFormat format:
+ *
+ * <ul>
+ *   <li>Binary value: &lt;version_id&gt;&lt;version_format&gt;
+ *   <li>version_id: 1-byte representing the version id.
+ *   <li>For version_id = 0:
+ *       <ul>
+ *         <li>version_format: &lt;field&gt;&lt;field&gt;
+ *         <li>field_format: &lt;field_id&gt;&lt;field_format&gt;
+ *         <li>Fields:
+ *             <ul>
+ *               <li>TraceId: (field_id = 0, len = 16, default = &#34;0000000000000000&#34;) -
+ *                   16-byte array representing the trace_id.
+ *               <li>SpanId: (field_id = 1, len = 8, default = &#34;00000000&#34;) - 8-byte array
+ *                   representing the span_id.
+ *               <li>TraceOptions: (field_id = 2, len = 1, default = &#34;0&#34;) - 1-byte array
+ *                   representing the trace_options.
+ *             </ul>
+ *         <li>Fields MUST be encoded using the field id order (smaller to higher).
+ *         <li>Valid value example:
+ *             <ul>
+ *               <li>{0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97,
+ *                   98, 99, 100, 101, 102, 103, 104, 2, 1}
+ *               <li>version_id = 0;
+ *               <li>trace_id = {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}
+ *               <li>span_id = {97, 98, 99, 100, 101, 102, 103, 104};
+ *               <li>trace_options = {1};
+ *             </ul>
+ *       </ul>
+ * </ul>
+ */
+final class BinaryFormatImpl extends BinaryFormat {
+  private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+  private static final byte VERSION_ID = 0;
+  private static final int VERSION_ID_OFFSET = 0;
+  // The version_id/field_id size in bytes.
+  private static final byte ID_SIZE = 1;
+  private static final byte TRACE_ID_FIELD_ID = 0;
+
+  // TODO: clarify if offsets are correct here. While the specification suggests you should stop
+  // parsing when you hit an unknown field, it does not suggest that fields must be declared in
+  // ID order. Rather it only groups by data type order, in this case Trace Context
+  // https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/BinaryEncoding.md#deserialization-rules
+  @VisibleForTesting static final int TRACE_ID_FIELD_ID_OFFSET = VERSION_ID_OFFSET + ID_SIZE;
+
+  private static final int TRACE_ID_OFFSET = TRACE_ID_FIELD_ID_OFFSET + ID_SIZE;
+  private static final byte SPAN_ID_FIELD_ID = 1;
+
+  @VisibleForTesting static final int SPAN_ID_FIELD_ID_OFFSET = TRACE_ID_OFFSET + TraceId.SIZE;
+
+  private static final int SPAN_ID_OFFSET = SPAN_ID_FIELD_ID_OFFSET + ID_SIZE;
+  private static final byte TRACE_OPTION_FIELD_ID = 2;
+
+  @VisibleForTesting static final int TRACE_OPTION_FIELD_ID_OFFSET = SPAN_ID_OFFSET + SpanId.SIZE;
+
+  private static final int TRACE_OPTIONS_OFFSET = TRACE_OPTION_FIELD_ID_OFFSET + ID_SIZE;
+  /** Version, Trace and Span IDs are required fields. */
+  private static final int REQUIRED_FORMAT_LENGTH = 3 * ID_SIZE + TraceId.SIZE + SpanId.SIZE;
+  /** Use {@link TraceOptions#DEFAULT} unless its optional field is present. */
+  private static final int ALL_FORMAT_LENGTH = REQUIRED_FORMAT_LENGTH + ID_SIZE + TraceOptions.SIZE;
+
+  @Override
+  public byte[] toByteArray(SpanContext spanContext) {
+    checkNotNull(spanContext, "spanContext");
+    byte[] bytes = new byte[ALL_FORMAT_LENGTH];
+    bytes[VERSION_ID_OFFSET] = VERSION_ID;
+    bytes[TRACE_ID_FIELD_ID_OFFSET] = TRACE_ID_FIELD_ID;
+    spanContext.getTraceId().copyBytesTo(bytes, TRACE_ID_OFFSET);
+    bytes[SPAN_ID_FIELD_ID_OFFSET] = SPAN_ID_FIELD_ID;
+    spanContext.getSpanId().copyBytesTo(bytes, SPAN_ID_OFFSET);
+    bytes[TRACE_OPTION_FIELD_ID_OFFSET] = TRACE_OPTION_FIELD_ID;
+    spanContext.getTraceOptions().copyBytesTo(bytes, TRACE_OPTIONS_OFFSET);
+    return bytes;
+  }
+
+  @Override
+  public SpanContext fromByteArray(byte[] bytes) throws SpanContextParseException {
+    checkNotNull(bytes, "bytes");
+    if (bytes.length == 0 || bytes[0] != VERSION_ID) {
+      throw new SpanContextParseException("Unsupported version.");
+    }
+    if (bytes.length < REQUIRED_FORMAT_LENGTH) {
+      throw new SpanContextParseException("Invalid input: truncated");
+    }
+    // TODO: the following logic assumes that fields are written in ID order. The spec does not say
+    // that. If it decides not to, this logic would need to be more like a loop
+    TraceId traceId;
+    SpanId spanId;
+    TraceOptions traceOptions = TraceOptions.DEFAULT;
+    int pos = 1;
+    if (bytes[pos] == TRACE_ID_FIELD_ID) {
+      traceId = TraceId.fromBytes(bytes, pos + ID_SIZE);
+      pos += ID_SIZE + TraceId.SIZE;
+    } else {
+      // TODO: update the spec to suggest that the trace ID is not actually optional
+      throw new SpanContextParseException("Invalid input: expected trace ID at offset " + pos);
+    }
+    if (bytes[pos] == SPAN_ID_FIELD_ID) {
+      spanId = SpanId.fromBytes(bytes, pos + ID_SIZE);
+      pos += ID_SIZE + SpanId.SIZE;
+    } else {
+      // TODO: update the spec to suggest that the span ID is not actually optional.
+      throw new SpanContextParseException("Invalid input: expected span ID at offset " + pos);
+    }
+    // Check to see if we are long enough to include an options field, and also that the next field
+    // is an options field. Per spec we simply stop parsing at first unknown field instead of
+    // failing.
+    if (bytes.length > pos && bytes[pos] == TRACE_OPTION_FIELD_ID) {
+      if (bytes.length < ALL_FORMAT_LENGTH) {
+        throw new SpanContextParseException("Invalid input: truncated");
+      }
+      traceOptions = TraceOptions.fromByte(bytes[pos + ID_SIZE]);
+    }
+    return SpanContext.create(traceId, spanId, traceOptions, TRACESTATE_DEFAULT);
+  }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/PropagationComponentImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/PropagationComponentImpl.java
new file mode 100644
index 0000000..f608543
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/PropagationComponentImpl.java
@@ -0,0 +1,37 @@
+/*
+ * 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.implcore.trace.propagation;
+
+import io.opencensus.trace.propagation.BinaryFormat;
+import io.opencensus.trace.propagation.PropagationComponent;
+import io.opencensus.trace.propagation.TextFormat;
+
+/** Implementation of the {@link PropagationComponent}. */
+public class PropagationComponentImpl extends PropagationComponent {
+  private final BinaryFormat binaryFormat = new BinaryFormatImpl();
+  private final B3Format b3Format = new B3Format();
+
+  @Override
+  public BinaryFormat getBinaryFormat() {
+    return binaryFormat;
+  }
+
+  @Override
+  public TextFormat getB3Format() {
+    return b3Format;
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/internal/CurrentStateTest.java b/impl_core/src/test/java/io/opencensus/implcore/internal/CurrentStateTest.java
new file mode 100644
index 0000000..b7e6a93
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/internal/CurrentStateTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018, 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.implcore.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.internal.CurrentState.State;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link CurrentState}. */
+@RunWith(JUnit4.class)
+public final class CurrentStateTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void defaultState() {
+    assertThat(new CurrentState(State.ENABLED).get()).isEqualTo(State.ENABLED);
+  }
+
+  @Test
+  public void setState() {
+    CurrentState currentState = new CurrentState(State.ENABLED);
+    assertThat(currentState.set(State.DISABLED)).isTrue();
+    assertThat(currentState.getInternal()).isEqualTo(State.DISABLED);
+    assertThat(currentState.set(State.ENABLED)).isTrue();
+    assertThat(currentState.getInternal()).isEqualTo(State.ENABLED);
+    assertThat(currentState.set(State.ENABLED)).isFalse();
+  }
+
+  @Test
+  public void preventSettingStateAfterReadingState() {
+    CurrentState currentState = new CurrentState(State.ENABLED);
+    currentState.get();
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("State was already read, cannot set state.");
+    currentState.set(State.DISABLED);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/internal/TimestampConverterTest.java b/impl_core/src/test/java/io/opencensus/implcore/internal/TimestampConverterTest.java
new file mode 100644
index 0000000..32a3e68
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/internal/TimestampConverterTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.implcore.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link TimestampConverter}. */
+@RunWith(JUnit4.class)
+public class TimestampConverterTest {
+  private final Timestamp timestamp = Timestamp.create(1234, 5678);
+  @Mock private Clock mockClock;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void convertNanoTime() {
+    when(mockClock.now()).thenReturn(timestamp);
+    when(mockClock.nowNanos()).thenReturn(1234L);
+    TimestampConverter timeConverter = TimestampConverter.now(mockClock);
+    assertThat(timeConverter.convertNanoTime(6234)).isEqualTo(Timestamp.create(1234, 10678));
+    assertThat(timeConverter.convertNanoTime(1000)).isEqualTo(Timestamp.create(1234, 5444));
+    assertThat(timeConverter.convertNanoTime(999995556)).isEqualTo(Timestamp.create(1235, 0));
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/internal/UtilsTest.java b/impl_core/src/test/java/io/opencensus/implcore/internal/UtilsTest.java
new file mode 100644
index 0000000..2e0bde2
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/internal/UtilsTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018, 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.implcore.internal;
+
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Utils}. */
+@RunWith(JUnit4.class)
+public class UtilsTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void checkListElementNull() {
+    List<Double> list = Arrays.asList(0.0, 1.0, 2.0, null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("null");
+    Utils.checkListElementNotNull(list, null);
+  }
+
+  @Test
+  public void checkListElementNull_WithMessage() {
+    List<Double> list = Arrays.asList(0.0, 1.0, 2.0, null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("list should not be null.");
+    Utils.checkListElementNotNull(list, "list should not be null.");
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImplTest.java
new file mode 100644
index 0000000..e69a284
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImplTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DerivedDoubleGaugeImpl}. */
+@RunWith(JUnit4.class)
+public class DerivedDoubleGaugeImplTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String METRIC_NAME = "name";
+  private static final String METRIC_DESCRIPTION = "description";
+  private static final String METRIC_UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+  private static final List<LabelValue> LABEL_VALUES_1 =
+      Collections.singletonList(LabelValue.create("value1"));
+  private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+  private final TestClock testClock = TestClock.create(TEST_TIME);
+  private static final MetricDescriptor METRIC_DESCRIPTOR =
+      MetricDescriptor.create(
+          METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, Type.GAUGE_DOUBLE, LABEL_KEY);
+
+  private final DerivedDoubleGaugeImpl derivedDoubleGauge =
+      new DerivedDoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, LABEL_KEY);
+
+  // helper class
+  public static class QueueManager {
+    public double size() {
+      return 2.5;
+    }
+  }
+
+  private static final ToDoubleFunction<Object> doubleFunction =
+      new ToDoubleFunction<Object>() {
+        @Override
+        public double applyAsDouble(Object value) {
+          return 5.5;
+        }
+      };
+  private static final ToDoubleFunction<Object> negativeDoubleFunction =
+      new ToDoubleFunction<Object>() {
+        @Override
+        public double applyAsDouble(Object value) {
+          return -200.5;
+        }
+      };
+  private static final ToDoubleFunction<QueueManager> queueManagerFunction =
+      new ToDoubleFunction<QueueManager>() {
+        @Override
+        public double applyAsDouble(QueueManager queue) {
+          return queue.size();
+        }
+      };
+
+  @Test
+  public void createTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    derivedDoubleGauge.createTimeSeries(null, null, doubleFunction);
+  }
+
+  @Test
+  public void createTimeSeries_WithNullElement() {
+    List<LabelKey> labelKeys =
+        Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+    List<LabelValue> labelValues = Arrays.asList(LabelValue.create("value1"), null);
+    DerivedDoubleGaugeImpl derivedDoubleGauge =
+        new DerivedDoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValue element should not be null.");
+    derivedDoubleGauge.createTimeSeries(labelValues, null, doubleFunction);
+  }
+
+  @Test
+  public void createTimeSeries_WithInvalidLabelSize() {
+    List<LabelValue> labelValues =
+        Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Incorrect number of labels.");
+    derivedDoubleGauge.createTimeSeries(labelValues, null, doubleFunction);
+  }
+
+  @Test
+  public void createTimeSeries_WithNullFunction() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("function");
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, null);
+  }
+
+  @Test
+  public void createTimeSeries_WithObjFunction() {
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, new QueueManager(), queueManagerFunction);
+    Metric metric = derivedDoubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric)
+        .isEqualTo(
+            Metric.createWithOneTimeSeries(
+                METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.doubleValue(2.5), TEST_TIME), null)));
+  }
+
+  @Test
+  public void createTimeSeries_WithSameLabel() {
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, new QueueManager(), queueManagerFunction);
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("A different time series with the same labels already exists.");
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, queueManagerFunction);
+  }
+
+  @Test
+  public void addTimeSeries_WithNullObj() {
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, negativeDoubleFunction);
+    Metric metric = derivedDoubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric)
+        .isEqualTo(
+            Metric.createWithOneTimeSeries(
+                METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.doubleValue(-200.5), TEST_TIME), null)));
+  }
+
+  @Test
+  public void removeTimeSeries() {
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+    Metric metric = derivedDoubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+    derivedDoubleGauge.removeTimeSeries(LABEL_VALUES);
+    assertThat(derivedDoubleGauge.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void removeTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    derivedDoubleGauge.removeTimeSeries(null);
+  }
+
+  @Test
+  public void multipleMetrics_GetMetric() {
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES_1, new QueueManager(), queueManagerFunction);
+    List<TimeSeries> expectedTimeSeriesList = new ArrayList<TimeSeries>();
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            LABEL_VALUES, Point.create(Value.doubleValue(5.5), TEST_TIME), null));
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            LABEL_VALUES_1, Point.create(Value.doubleValue(2.5), TEST_TIME), null));
+    Metric metric = derivedDoubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+    assertThat(metric.getTimeSeriesList()).containsExactlyElementsIn(expectedTimeSeriesList);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(0))
+        .isEqualTo(LabelValue.create("value"));
+    assertThat(metric.getTimeSeriesList().get(1).getLabelValues().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(1).getLabelValues().get(0))
+        .isEqualTo(LabelValue.create("value1"));
+  }
+
+  @Test
+  public void clear() {
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES_1, new QueueManager(), queueManagerFunction);
+    Metric metric = derivedDoubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+    derivedDoubleGauge.clear();
+    assertThat(derivedDoubleGauge.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void empty_GetMetrics() {
+    assertThat(derivedDoubleGauge.getMetric(testClock)).isNull();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedLongGaugeImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedLongGaugeImplTest.java
new file mode 100644
index 0000000..ec9cad6
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedLongGaugeImplTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DerivedLongGaugeImpl}. */
+@RunWith(JUnit4.class)
+public class DerivedLongGaugeImplTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String METRIC_NAME = "name";
+  private static final String METRIC_DESCRIPTION = "description";
+  private static final String METRIC_UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+  private static final List<LabelValue> LABEL_VALUES_1 =
+      Collections.singletonList(LabelValue.create("value1"));
+
+  private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+  private final TestClock testClock = TestClock.create(TEST_TIME);
+
+  private static final MetricDescriptor METRIC_DESCRIPTOR =
+      MetricDescriptor.create(
+          METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, Type.GAUGE_INT64, LABEL_KEY);
+
+  private final DerivedLongGaugeImpl derivedLongGauge =
+      new DerivedLongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, LABEL_KEY);
+
+  // helper class
+  public static class QueueManager {
+    public long size() {
+      return 2;
+    }
+  }
+
+  private static final ToLongFunction<Object> longFunction =
+      new ToLongFunction<Object>() {
+        @Override
+        public long applyAsLong(Object value) {
+          return 5;
+        }
+      };
+  private static final ToLongFunction<Object> negativeLongFunction =
+      new ToLongFunction<Object>() {
+        @Override
+        public long applyAsLong(Object value) {
+          return -200;
+        }
+      };
+  private static final ToLongFunction<QueueManager> queueManagerFunction =
+      new ToLongFunction<QueueManager>() {
+        @Override
+        public long applyAsLong(QueueManager queue) {
+          return queue.size();
+        }
+      };
+
+  @Test
+  public void createTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    derivedLongGauge.createTimeSeries(null, null, longFunction);
+  }
+
+  @Test
+  public void createTimeSeries_WithNullElement() {
+    List<LabelKey> labelKeys =
+        Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+    List<LabelValue> labelValues = Arrays.asList(LabelValue.create("value1"), null);
+
+    DerivedLongGaugeImpl derivedLongGauge =
+        new DerivedLongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValue element should not be null.");
+    derivedLongGauge.createTimeSeries(labelValues, null, longFunction);
+  }
+
+  @Test
+  public void createTimeSeries_WithInvalidLabelSize() {
+    List<LabelValue> labelValues =
+        Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Incorrect number of labels.");
+    derivedLongGauge.createTimeSeries(labelValues, null, longFunction);
+  }
+
+  @Test
+  public void createTimeSeries_WithNullFunction() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("function");
+    derivedLongGauge.createTimeSeries(LABEL_VALUES, null, null);
+  }
+
+  @Test
+  public void createTimeSeries_WithObjFunction() {
+    derivedLongGauge.createTimeSeries(LABEL_VALUES, new QueueManager(), queueManagerFunction);
+
+    Metric metric = derivedLongGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric)
+        .isEqualTo(
+            Metric.createWithOneTimeSeries(
+                METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.longValue(2), TEST_TIME), null)));
+  }
+
+  @Test
+  public void addTimeSeries_WithNullObj() {
+    derivedLongGauge.createTimeSeries(LABEL_VALUES, null, negativeLongFunction);
+
+    Metric metric = derivedLongGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric)
+        .isEqualTo(
+            Metric.createWithOneTimeSeries(
+                METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.longValue(-200), TEST_TIME), null)));
+  }
+
+  @Test
+  public void removeTimeSeries() {
+    derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+    Metric metric = derivedLongGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+
+    derivedLongGauge.removeTimeSeries(LABEL_VALUES);
+    assertThat(derivedLongGauge.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void removeTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    derivedLongGauge.removeTimeSeries(null);
+  }
+
+  @Test
+  public void multipleMetrics_GetMetric() {
+    derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+    derivedLongGauge.createTimeSeries(LABEL_VALUES_1, new QueueManager(), queueManagerFunction);
+
+    List<TimeSeries> expectedTimeSeriesList = new ArrayList<TimeSeries>();
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            LABEL_VALUES, Point.create(Value.longValue(5), TEST_TIME), null));
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            LABEL_VALUES_1, Point.create(Value.longValue(2), TEST_TIME), null));
+
+    Metric metric = derivedLongGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+    assertThat(metric.getTimeSeriesList()).containsExactlyElementsIn(expectedTimeSeriesList);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(0))
+        .isEqualTo(LabelValue.create("value"));
+    assertThat(metric.getTimeSeriesList().get(1).getLabelValues().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(1).getLabelValues().get(0))
+        .isEqualTo(LabelValue.create("value1"));
+  }
+
+  @Test
+  public void clear() {
+    derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+    derivedLongGauge.createTimeSeries(LABEL_VALUES_1, new QueueManager(), queueManagerFunction);
+
+    Metric metric = derivedLongGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+
+    derivedLongGauge.clear();
+    assertThat(derivedLongGauge.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void empty_GetMetrics() {
+    assertThat(derivedLongGauge.getMetric(testClock)).isNull();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/DoubleGaugeImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/DoubleGaugeImplTest.java
new file mode 100644
index 0000000..b089908
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/DoubleGaugeImplTest.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.metrics.DoubleGaugeImpl.UNSET_VALUE;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.DoubleGauge.DoublePoint;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DoubleGaugeImpl}. */
+@RunWith(JUnit4.class)
+public class DoubleGaugeImplTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String METRIC_NAME = "name";
+  private static final String METRIC_DESCRIPTION = "description";
+  private static final String METRIC_UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+  private static final List<LabelValue> LABEL_VALUES1 =
+      Collections.singletonList(LabelValue.create("value1"));
+  private static final List<LabelValue> DEFAULT_LABEL_VALUES =
+      Collections.singletonList(UNSET_VALUE);
+
+  private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+  private final TestClock testClock = TestClock.create(TEST_TIME);
+  private static final MetricDescriptor METRIC_DESCRIPTOR =
+      MetricDescriptor.create(
+          METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, Type.GAUGE_DOUBLE, LABEL_KEY);
+  private final DoubleGaugeImpl doubleGauge =
+      new DoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, LABEL_KEY);
+
+  @Test
+  public void getOrCreateTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    doubleGauge.getOrCreateTimeSeries(null);
+  }
+
+  @Test
+  public void getOrCreateTimeSeries_WithNullElement() {
+    List<LabelKey> labelKeys =
+        Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+    List<LabelValue> labelValues = Arrays.asList(LabelValue.create("value1"), null);
+
+    DoubleGaugeImpl doubleGauge =
+        new DoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValue element should not be null.");
+    doubleGauge.getOrCreateTimeSeries(labelValues);
+  }
+
+  @Test
+  public void getOrCreateTimeSeries_WithInvalidLabelSize() {
+    List<LabelValue> labelValues =
+        Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Incorrect number of labels.");
+    doubleGauge.getOrCreateTimeSeries(labelValues);
+  }
+
+  @Test
+  public void getOrCreateTimeSeries() {
+    DoublePoint point = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    point.add(100);
+    DoublePoint point1 = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    point1.set(500);
+
+    Metric metric = doubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric)
+        .isEqualTo(
+            Metric.create(
+                METRIC_DESCRIPTOR,
+                Collections.singletonList(
+                    TimeSeries.createWithOnePoint(
+                        LABEL_VALUES, Point.create(Value.doubleValue(500), TEST_TIME), null))));
+    assertThat(point).isSameAs(point1);
+  }
+
+  @Test
+  public void getOrCreateTimeSeries_WithNegativePointValues() {
+    DoublePoint point = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    point.add(-100);
+    point.add(-33);
+
+    Metric metric = doubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(Value.doubleValue(-133));
+    assertThat(metric.getTimeSeriesList().get(0).getPoints().get(0).getTimestamp())
+        .isEqualTo(TEST_TIME);
+    assertThat(metric.getTimeSeriesList().get(0).getStartTimestamp()).isNull();
+  }
+
+  @Test
+  public void getDefaultTimeSeries() {
+    DoublePoint point = doubleGauge.getDefaultTimeSeries();
+    point.add(100);
+    point.set(500);
+
+    DoublePoint point1 = doubleGauge.getDefaultTimeSeries();
+    point1.add(-100);
+
+    Metric metric = doubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric)
+        .isEqualTo(
+            Metric.create(
+                METRIC_DESCRIPTOR,
+                Collections.singletonList(
+                    TimeSeries.createWithOnePoint(
+                        DEFAULT_LABEL_VALUES,
+                        Point.create(Value.doubleValue(400), TEST_TIME),
+                        null))));
+    assertThat(point).isSameAs(point1);
+  }
+
+  @Test
+  public void removeTimeSeries() {
+    doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    assertThat(doubleGauge.getMetric(testClock))
+        .isEqualTo(
+            Metric.create(
+                METRIC_DESCRIPTOR,
+                Collections.singletonList(
+                    TimeSeries.createWithOnePoint(
+                        LABEL_VALUES, Point.create(Value.doubleValue(0), TEST_TIME), null))));
+
+    doubleGauge.removeTimeSeries(LABEL_VALUES);
+    assertThat(doubleGauge.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void removeTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    doubleGauge.removeTimeSeries(null);
+  }
+
+  @Test
+  public void clear() {
+    DoublePoint doublePoint = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    doublePoint.add(-11);
+    DoublePoint defaultPoint = doubleGauge.getDefaultTimeSeries();
+    defaultPoint.set(100);
+
+    Metric metric = doubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+
+    doubleGauge.clear();
+    assertThat(doubleGauge.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void setDefaultLabelValues() {
+    List<LabelKey> labelKeys =
+        Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+    DoubleGaugeImpl doubleGauge =
+        new DoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+    DoublePoint defaultPoint = doubleGauge.getDefaultTimeSeries();
+    defaultPoint.set(-230);
+
+    Metric metric = doubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(2);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(0)).isEqualTo(UNSET_VALUE);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(1)).isEqualTo(UNSET_VALUE);
+  }
+
+  @Test
+  public void pointImpl_InstanceOf() {
+    DoublePoint doublePoint = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    assertThat(doublePoint).isInstanceOf(DoubleGaugeImpl.PointImpl.class);
+  }
+
+  @Test
+  public void multipleMetrics_GetMetric() {
+    DoublePoint doublePoint = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    doublePoint.add(1);
+    doublePoint.add(2);
+
+    DoublePoint defaultPoint = doubleGauge.getDefaultTimeSeries();
+    defaultPoint.set(100);
+
+    DoublePoint doublePoint1 = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES1);
+    doublePoint1.add(-100);
+    doublePoint1.add(-20);
+
+    List<TimeSeries> expectedTimeSeriesList = new ArrayList<TimeSeries>();
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            LABEL_VALUES, Point.create(Value.doubleValue(3), TEST_TIME), null));
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            DEFAULT_LABEL_VALUES, Point.create(Value.doubleValue(100), TEST_TIME), null));
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            LABEL_VALUES1, Point.create(Value.doubleValue(-120), TEST_TIME), null));
+
+    Metric metric = doubleGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(3);
+    assertThat(metric.getTimeSeriesList()).containsExactlyElementsIn(expectedTimeSeriesList);
+  }
+
+  @Test
+  public void empty_GetMetrics() {
+    assertThat(doubleGauge.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void testEquals() {
+    List<LabelKey> labelKeys =
+        Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+    List<LabelValue> labelValues =
+        Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+    DoubleGaugeImpl doubleGauge =
+        new DoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+
+    DoublePoint defaultPoint1 = doubleGauge.getDefaultTimeSeries();
+    DoublePoint defaultPoint2 = doubleGauge.getDefaultTimeSeries();
+    DoublePoint doublePoint1 = doubleGauge.getOrCreateTimeSeries(labelValues);
+    DoublePoint doublePoint2 = doubleGauge.getOrCreateTimeSeries(labelValues);
+
+    new EqualsTester()
+        .addEqualityGroup(defaultPoint1, defaultPoint2)
+        .addEqualityGroup(doublePoint1, doublePoint2)
+        .testEquals();
+
+    doubleGauge.clear();
+
+    DoublePoint newDefaultPointAfterClear = doubleGauge.getDefaultTimeSeries();
+    DoublePoint newDoublePointAfterClear = doubleGauge.getOrCreateTimeSeries(labelValues);
+
+    doubleGauge.removeTimeSeries(labelValues);
+    DoublePoint newDoublePointAfterRemove = doubleGauge.getOrCreateTimeSeries(labelValues);
+
+    new EqualsTester()
+        .addEqualityGroup(defaultPoint1, defaultPoint2)
+        .addEqualityGroup(doublePoint1, doublePoint2)
+        .addEqualityGroup(newDefaultPointAfterClear)
+        .addEqualityGroup(newDoublePointAfterClear)
+        .addEqualityGroup(newDoublePointAfterRemove)
+        .testEquals();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/LongGaugeImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/LongGaugeImplTest.java
new file mode 100644
index 0000000..e83bb64
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/LongGaugeImplTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.metrics.LongGaugeImpl.UNSET_VALUE;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.LongGauge.LongPoint;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link LongGaugeImpl}. */
+@RunWith(JUnit4.class)
+public class LongGaugeImplTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String METRIC_NAME = "name";
+  private static final String METRIC_DESCRIPTION = "description";
+  private static final String METRIC_UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+  private static final List<LabelValue> LABEL_VALUES1 =
+      Collections.singletonList(LabelValue.create("value1"));
+  private static final List<LabelValue> DEFAULT_LABEL_VALUES =
+      Collections.singletonList(UNSET_VALUE);
+
+  private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+  private final TestClock testClock = TestClock.create(TEST_TIME);
+  private static final MetricDescriptor METRIC_DESCRIPTOR =
+      MetricDescriptor.create(
+          METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, Type.GAUGE_INT64, LABEL_KEY);
+  private final LongGaugeImpl longGaugeMetric =
+      new LongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, LABEL_KEY);
+
+  @Test
+  public void getOrCreateTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    longGaugeMetric.getOrCreateTimeSeries(null);
+  }
+
+  @Test
+  public void getOrCreateTimeSeries_WithNullElement() {
+    List<LabelKey> labelKeys =
+        Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+    List<LabelValue> labelValues = Arrays.asList(LabelValue.create("value1"), null);
+
+    LongGaugeImpl longGauge =
+        new LongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValue element should not be null.");
+    longGauge.getOrCreateTimeSeries(labelValues);
+  }
+
+  @Test
+  public void getOrCreateTimeSeries_WithInvalidLabelSize() {
+    List<LabelValue> labelValues =
+        Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Incorrect number of labels.");
+    longGaugeMetric.getOrCreateTimeSeries(labelValues);
+  }
+
+  @Test
+  public void getOrCreateTimeSeries() {
+    LongPoint point = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+    point.add(100);
+    LongPoint point1 = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+    point1.set(500);
+
+    Metric metric = longGaugeMetric.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric)
+        .isEqualTo(
+            Metric.createWithOneTimeSeries(
+                METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.longValue(500), TEST_TIME), null)));
+    assertThat(point).isSameAs(point1);
+  }
+
+  @Test
+  public void getOrCreateTimeSeries_WithNegativePointValues() {
+    LongPoint point = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+    point.add(-100);
+    point.add(-33);
+
+    Metric metric = longGaugeMetric.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(0).getPoints().get(0).getValue())
+        .isEqualTo(Value.longValue(-133));
+    assertThat(metric.getTimeSeriesList().get(0).getPoints().get(0).getTimestamp())
+        .isEqualTo(TEST_TIME);
+    assertThat(metric.getTimeSeriesList().get(0).getStartTimestamp()).isNull();
+  }
+
+  @Test
+  public void getDefaultTimeSeries() {
+    LongPoint point = longGaugeMetric.getDefaultTimeSeries();
+    point.add(100);
+    point.set(500);
+
+    LongPoint point1 = longGaugeMetric.getDefaultTimeSeries();
+    point1.add(-100);
+
+    Metric metric = longGaugeMetric.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric)
+        .isEqualTo(
+            Metric.createWithOneTimeSeries(
+                METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    DEFAULT_LABEL_VALUES, Point.create(Value.longValue(400), TEST_TIME), null)));
+    assertThat(point).isSameAs(point1);
+  }
+
+  @Test
+  public void removeTimeSeries() {
+    longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+    assertThat(longGaugeMetric.getMetric(testClock))
+        .isEqualTo(
+            Metric.createWithOneTimeSeries(
+                METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.longValue(0), TEST_TIME), null)));
+
+    longGaugeMetric.removeTimeSeries(LABEL_VALUES);
+    assertThat(longGaugeMetric.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void removeTimeSeries_WithNullLabelValues() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelValues");
+    longGaugeMetric.removeTimeSeries(null);
+  }
+
+  @Test
+  public void clear() {
+    LongPoint longPoint = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+    longPoint.add(-11);
+    LongPoint defaultPoint = longGaugeMetric.getDefaultTimeSeries();
+    defaultPoint.set(100);
+
+    Metric metric = longGaugeMetric.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+
+    longGaugeMetric.clear();
+    assertThat(longGaugeMetric.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void setDefaultLabelValues() {
+    List<LabelKey> labelKeys =
+        Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+    LongGaugeImpl longGauge =
+        new LongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+    LongPoint defaultPoint = longGauge.getDefaultTimeSeries();
+    defaultPoint.set(-230);
+
+    Metric metric = longGauge.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(2);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(0)).isEqualTo(UNSET_VALUE);
+    assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(1)).isEqualTo(UNSET_VALUE);
+  }
+
+  @Test
+  public void pointImpl_InstanceOf() {
+    LongPoint longPoint = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+    assertThat(longPoint).isInstanceOf(LongGaugeImpl.PointImpl.class);
+  }
+
+  @Test
+  public void multipleMetrics_GetMetric() {
+    LongPoint longPoint = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+    longPoint.add(1);
+    longPoint.add(2);
+
+    LongPoint defaultPoint = longGaugeMetric.getDefaultTimeSeries();
+    defaultPoint.set(100);
+
+    LongPoint longPoint1 = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES1);
+    longPoint1.add(-100);
+    longPoint1.add(-20);
+
+    List<TimeSeries> expectedTimeSeriesList = new ArrayList<TimeSeries>();
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            LABEL_VALUES, Point.create(Value.longValue(3), TEST_TIME), null));
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            DEFAULT_LABEL_VALUES, Point.create(Value.longValue(100), TEST_TIME), null));
+    expectedTimeSeriesList.add(
+        TimeSeries.createWithOnePoint(
+            LABEL_VALUES1, Point.create(Value.longValue(-120), TEST_TIME), null));
+
+    Metric metric = longGaugeMetric.getMetric(testClock);
+    assertThat(metric).isNotNull();
+    assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+    assertThat(metric.getTimeSeriesList().size()).isEqualTo(3);
+    assertThat(metric.getTimeSeriesList()).containsExactlyElementsIn(expectedTimeSeriesList);
+  }
+
+  @Test
+  public void empty_GetMetrics() {
+    assertThat(longGaugeMetric.getMetric(testClock)).isNull();
+  }
+
+  @Test
+  public void testEquals() {
+    List<LabelKey> labelKeys =
+        Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+    List<LabelValue> labelValues =
+        Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+    LongGaugeImpl longGauge =
+        new LongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+
+    LongPoint defaultPoint1 = longGauge.getDefaultTimeSeries();
+    LongPoint defaultPoint2 = longGauge.getDefaultTimeSeries();
+    LongPoint longPoint1 = longGauge.getOrCreateTimeSeries(labelValues);
+    LongPoint longPoint2 = longGauge.getOrCreateTimeSeries(labelValues);
+
+    new EqualsTester()
+        .addEqualityGroup(defaultPoint1, defaultPoint2)
+        .addEqualityGroup(longPoint1, longPoint2)
+        .testEquals();
+
+    longGauge.clear();
+
+    LongPoint newDefaultPointAfterClear = longGauge.getDefaultTimeSeries();
+    LongPoint newLongPointAfterClear = longGauge.getOrCreateTimeSeries(labelValues);
+
+    longGauge.removeTimeSeries(labelValues);
+    LongPoint newLongPointAfterRemove = longGauge.getOrCreateTimeSeries(labelValues);
+
+    new EqualsTester()
+        .addEqualityGroup(defaultPoint1, defaultPoint2)
+        .addEqualityGroup(longPoint1, longPoint2)
+        .addEqualityGroup(newDefaultPointAfterClear)
+        .addEqualityGroup(newLongPointAfterClear)
+        .addEqualityGroup(newLongPointAfterRemove)
+        .testEquals();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricRegistryImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricRegistryImplTest.java
new file mode 100644
index 0000000..68bfda3
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricRegistryImplTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.metrics.DerivedDoubleGauge;
+import io.opencensus.metrics.DerivedLongGauge;
+import io.opencensus.metrics.DoubleGauge;
+import io.opencensus.metrics.DoubleGauge.DoublePoint;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.LongGauge;
+import io.opencensus.metrics.LongGauge.LongPoint;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MetricRegistryImpl}. */
+@RunWith(JUnit4.class)
+public class MetricRegistryImplTest {
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final String NAME = "name";
+  private static final String NAME_2 = "name2";
+  private static final String NAME_3 = "name3";
+  private static final String NAME_4 = "name4";
+  private static final String DESCRIPTION = "description";
+  private static final String UNIT = "1";
+  private static final List<LabelKey> LABEL_KEY =
+      Collections.singletonList(LabelKey.create("key", "key description"));
+  private static final List<LabelValue> LABEL_VALUES =
+      Collections.singletonList(LabelValue.create("value"));
+
+  private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+  private final TestClock testClock = TestClock.create(TEST_TIME);
+  private final MetricRegistryImpl metricRegistry = new MetricRegistryImpl(testClock);
+
+  private static final MetricDescriptor LONG_METRIC_DESCRIPTOR =
+      MetricDescriptor.create(NAME, DESCRIPTION, UNIT, Type.GAUGE_INT64, LABEL_KEY);
+  private static final MetricDescriptor DOUBLE_METRIC_DESCRIPTOR =
+      MetricDescriptor.create(NAME_2, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, LABEL_KEY);
+  private static final MetricDescriptor DERIVED_LONG_METRIC_DESCRIPTOR =
+      MetricDescriptor.create(NAME_3, DESCRIPTION, UNIT, Type.GAUGE_INT64, LABEL_KEY);
+  private static final MetricDescriptor DERIVED_DOUBLE_METRIC_DESCRIPTOR =
+      MetricDescriptor.create(NAME_4, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, LABEL_KEY);
+
+  private static final ToLongFunction<Object> longFunction =
+      new ToLongFunction<Object>() {
+        @Override
+        public long applyAsLong(Object value) {
+          return 5;
+        }
+      };
+  private static final ToDoubleFunction<Object> doubleFunction =
+      new ToDoubleFunction<Object>() {
+        @Override
+        public double applyAsDouble(Object value) {
+          return 5.0;
+        }
+      };
+
+  @Test
+  public void addLongGauge_NullName() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("name");
+    metricRegistry.addLongGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void addLongGauge_NullDescription() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("description");
+    metricRegistry.addLongGauge(NAME, null, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void addLongGauge_NullUnit() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("unit");
+    metricRegistry.addLongGauge(NAME, DESCRIPTION, null, LABEL_KEY);
+  }
+
+  @Test
+  public void addLongGauge_NullLabels() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKeys");
+    metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, null);
+  }
+
+  @Test
+  public void addLongGauge_WithNullElement() {
+    List<LabelKey> labelKeys = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKey element should not be null.");
+    metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, labelKeys);
+  }
+
+  @Test
+  public void addDoubleGauge_NullName() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("name");
+    metricRegistry.addDoubleGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void addDoubleGauge_NullDescription() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("description");
+    metricRegistry.addDoubleGauge(NAME_2, null, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void addDoubleGauge_NullUnit() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("unit");
+    metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, null, LABEL_KEY);
+  }
+
+  @Test
+  public void addDoubleGauge_NullLabels() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKeys");
+    metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, null);
+  }
+
+  @Test
+  public void addDoubleGauge_WithNullElement() {
+    List<LabelKey> labelKeys = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKey element should not be null.");
+    metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, labelKeys);
+  }
+
+  @Test
+  public void addDerivedLongGauge_NullName() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("name");
+    metricRegistry.addDerivedLongGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void addDerivedLongGauge_NullDescription() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("description");
+    metricRegistry.addDerivedLongGauge(NAME_3, null, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void addDerivedLongGauge_NullUnit() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("unit");
+    metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, null, LABEL_KEY);
+  }
+
+  @Test
+  public void addDerivedLongGauge_NullLabels() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKeys");
+    metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, null);
+  }
+
+  @Test
+  public void addDerivedLongGauge_WithNullElement() {
+    List<LabelKey> labelKeys = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKey element should not be null.");
+    metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, labelKeys);
+  }
+
+  @Test
+  public void addDerivedDoubleGauge_NullName() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("name");
+    metricRegistry.addDerivedDoubleGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void addDerivedDoubleGauge_NullDescription() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("description");
+    metricRegistry.addDerivedDoubleGauge(NAME_4, null, UNIT, LABEL_KEY);
+  }
+
+  @Test
+  public void addDerivedDoubleGauge_NullUnit() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("unit");
+    metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, null, LABEL_KEY);
+  }
+
+  @Test
+  public void addDerivedDoubleGauge_NullLabels() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKeys");
+    metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, null);
+  }
+
+  @Test
+  public void addDerivedDoubleGauge_WithNullElement() {
+    List<LabelKey> labelKeys = Collections.singletonList(null);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("labelKey element should not be null.");
+    metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, labelKeys);
+  }
+
+  @Test
+  public void addLongGauge_GetMetrics() {
+    LongGauge longGauge = metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    longGauge.getOrCreateTimeSeries(LABEL_VALUES);
+
+    Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+    assertThat(metricCollections.size()).isEqualTo(1);
+    assertThat(metricCollections)
+        .containsExactly(
+            Metric.createWithOneTimeSeries(
+                LONG_METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.longValue(0), TEST_TIME), null)));
+  }
+
+  @Test
+  public void addDoubleGauge_GetMetrics() {
+    DoubleGauge doubleGauge = metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY);
+    doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+    assertThat(metricCollections.size()).isEqualTo(1);
+    assertThat(metricCollections)
+        .containsExactly(
+            Metric.createWithOneTimeSeries(
+                DOUBLE_METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.doubleValue(0.0), TEST_TIME), null)));
+  }
+
+  @Test
+  public void addDerivedLongGauge_GetMetrics() {
+    DerivedLongGauge derivedLongGauge =
+        metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY);
+    derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+    Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+    assertThat(metricCollections.size()).isEqualTo(1);
+    assertThat(metricCollections)
+        .containsExactly(
+            Metric.createWithOneTimeSeries(
+                DERIVED_LONG_METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.longValue(5), TEST_TIME), null)));
+  }
+
+  @Test
+  public void addDerivedDoubleGauge_GetMetrics() {
+    DerivedDoubleGauge derivedDoubleGauge =
+        metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY);
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+    Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+    assertThat(metricCollections.size()).isEqualTo(1);
+    assertThat(metricCollections)
+        .containsExactly(
+            Metric.createWithOneTimeSeries(
+                DERIVED_DOUBLE_METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.doubleValue(5.0), TEST_TIME), null)));
+  }
+
+  @Test
+  public void empty_GetMetrics() {
+    assertThat(metricRegistry.getMetricProducer().getMetrics()).isEmpty();
+  }
+
+  @Test
+  public void checkInstanceOf() {
+    assertThat(metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY))
+        .isInstanceOf(LongGaugeImpl.class);
+    assertThat(metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY))
+        .isInstanceOf(DoubleGaugeImpl.class);
+    assertThat(metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY))
+        .isInstanceOf(DerivedLongGaugeImpl.class);
+    assertThat(metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY))
+        .isInstanceOf(DerivedDoubleGaugeImpl.class);
+  }
+
+  @Test
+  public void getMetrics() {
+    LongGauge longGauge = metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    LongPoint longPoint = longGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    longPoint.set(200);
+    DoubleGauge doubleGauge = metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY);
+    DoublePoint doublePoint = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+    doublePoint.set(-300.13);
+    DerivedLongGauge derivedLongGauge =
+        metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY);
+    derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+    DerivedDoubleGauge derivedDoubleGauge =
+        metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY);
+    derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+
+    Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+    assertThat(metricCollections.size()).isEqualTo(4);
+    assertThat(metricCollections)
+        .containsExactly(
+            Metric.createWithOneTimeSeries(
+                LONG_METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.longValue(200), TEST_TIME), null)),
+            Metric.createWithOneTimeSeries(
+                DOUBLE_METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.doubleValue(-300.13), TEST_TIME), null)),
+            Metric.createWithOneTimeSeries(
+                DERIVED_LONG_METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.longValue(5), TEST_TIME), null)),
+            Metric.createWithOneTimeSeries(
+                DERIVED_DOUBLE_METRIC_DESCRIPTOR,
+                TimeSeries.createWithOnePoint(
+                    LABEL_VALUES, Point.create(Value.doubleValue(5.0), TEST_TIME), null)));
+  }
+
+  @Test
+  public void registerDifferentMetricSameName() {
+    metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("A different metric with the same name already registered.");
+    metricRegistry.addDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricsComponentImplBaseTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricsComponentImplBaseTest.java
new file mode 100644
index 0000000..7f8515d
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricsComponentImplBaseTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2018, 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.implcore.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.metrics.export.ExportComponentImpl;
+import io.opencensus.testing.common.TestClock;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MetricsComponentImplBase}. */
+@RunWith(JUnit4.class)
+public class MetricsComponentImplBaseTest {
+  private final MetricsComponentImplBase metricsComponentImplBase =
+      new MetricsComponentImplBase(TestClock.create());
+
+  @Test
+  public void getExportComponent() {
+    assertThat(metricsComponentImplBase.getExportComponent())
+        .isInstanceOf(ExportComponentImpl.class);
+  }
+
+  @Test
+  public void getMetricRegistry() {
+    assertThat(metricsComponentImplBase.getMetricRegistry()).isInstanceOf(MetricRegistryImpl.class);
+  }
+
+  @Test
+  public void metricRegistry_InstalledToMetricProducerManger() {
+    assertThat(
+            metricsComponentImplBase
+                .getExportComponent()
+                .getMetricProducerManager()
+                .getAllMetricProducer())
+        .containsExactly(metricsComponentImplBase.getMetricRegistry().getMetricProducer());
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/export/ExportComponentImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/export/ExportComponentImplTest.java
new file mode 100644
index 0000000..fb91641
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/export/ExportComponentImplTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018, 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.implcore.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ExportComponentImpl}. */
+@RunWith(JUnit4.class)
+public class ExportComponentImplTest {
+
+  @Test
+  public void getMetricProducerManager() {
+    ExportComponentImpl exportComponent = new ExportComponentImpl();
+    assertThat(exportComponent.getMetricProducerManager())
+        .isInstanceOf(MetricProducerManagerImpl.class);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImplTest.java
new file mode 100644
index 0000000..e549dad
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImplTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2018, 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.implcore.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.metrics.export.MetricProducer;
+import io.opencensus.metrics.export.MetricProducerManager;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link MetricProducerManagerImpl}. */
+@RunWith(JUnit4.class)
+public class MetricProducerManagerImplTest {
+
+  private final MetricProducerManager metricProducerManager = new MetricProducerManagerImpl();
+  @Mock private MetricProducer metricProducer;
+  @Mock private MetricProducer metricProducerOther;
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void add_DisallowsNull() {
+    thrown.expect(NullPointerException.class);
+    metricProducerManager.add(null);
+  }
+
+  @Test
+  public void add() {
+    metricProducerManager.add(metricProducer);
+    assertThat(metricProducerManager.getAllMetricProducer()).containsExactly(metricProducer);
+  }
+
+  @Test
+  public void add_DuplicateElement() {
+    metricProducerManager.add(metricProducer);
+    Set<MetricProducer> metricProducerSet = metricProducerManager.getAllMetricProducer();
+    assertThat(metricProducerSet).containsExactly(metricProducer);
+    metricProducerManager.add(metricProducer);
+    // Returns the same object.
+    assertThat(metricProducerManager.getAllMetricProducer()).isSameAs(metricProducerSet);
+  }
+
+  @Test
+  public void add_MultipleElements() {
+    metricProducerManager.add(metricProducer);
+    Set<MetricProducer> metricProducerSet = metricProducerManager.getAllMetricProducer();
+    assertThat(metricProducerSet).containsExactly(metricProducer);
+    metricProducerManager.add(metricProducerOther);
+    // Returns the same object.
+    assertThat(metricProducerManager.getAllMetricProducer())
+        .containsExactly(metricProducer, metricProducerOther);
+  }
+
+  @Test
+  public void addAndRemove() {
+    metricProducerManager.add(metricProducer);
+    assertThat(metricProducerManager.getAllMetricProducer()).containsExactly(metricProducer);
+    metricProducerManager.remove(metricProducer);
+    assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+  }
+
+  @Test
+  public void remove_DisallowsNull() {
+    thrown.expect(NullPointerException.class);
+    metricProducerManager.remove(null);
+  }
+
+  @Test
+  public void remove_FromEmpty() {
+    metricProducerManager.remove(metricProducer);
+    assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+  }
+
+  @Test
+  public void remove_NotPresent() {
+    metricProducerManager.add(metricProducer);
+    Set<MetricProducer> metricProducerSet = metricProducerManager.getAllMetricProducer();
+    assertThat(metricProducerSet).containsExactly(metricProducer);
+    metricProducerManager.remove(metricProducerOther);
+    // Returns the same object.
+    assertThat(metricProducerManager.getAllMetricProducer()).isSameAs(metricProducerSet);
+  }
+
+  @Test
+  public void getAllMetricProducer_empty() {
+    assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/IntervalBucketTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/IntervalBucketTest.java
new file mode 100644
index 0000000..39a53e1
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/IntervalBucketTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.stats.MutableAggregation.MutableMean;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.tags.TagValue;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link IntervalBucket}. */
+@RunWith(JUnit4.class)
+public class IntervalBucketTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private static final double TOLERANCE = 1e-6;
+  private static final MeasureDouble MEASURE_DOUBLE =
+      MeasureDouble.create("measure1", "description", "1");
+  private static final Duration MINUTE = Duration.create(60, 0);
+  private static final Duration NEGATIVE_TEN_SEC = Duration.create(-10, 0);
+  private static final Timestamp START = Timestamp.create(60, 0);
+  private static final Mean MEAN = Mean.create();
+
+  @Test
+  public void preventNullStartTime() {
+    thrown.expect(NullPointerException.class);
+    new IntervalBucket(null, MINUTE, MEAN, MEASURE_DOUBLE);
+  }
+
+  @Test
+  public void preventNullDuration() {
+    thrown.expect(NullPointerException.class);
+    new IntervalBucket(START, null, MEAN, MEASURE_DOUBLE);
+  }
+
+  @Test
+  public void preventNegativeDuration() {
+    thrown.expect(IllegalArgumentException.class);
+    new IntervalBucket(START, NEGATIVE_TEN_SEC, MEAN, MEASURE_DOUBLE);
+  }
+
+  @Test
+  public void preventNullAggregation() {
+    thrown.expect(NullPointerException.class);
+    new IntervalBucket(START, MINUTE, null, MEASURE_DOUBLE);
+  }
+
+  @Test
+  public void preventNullMeasure() {
+    thrown.expect(NullPointerException.class);
+    new IntervalBucket(START, MINUTE, MEAN, null);
+  }
+
+  @Test
+  public void testGetTagValueAggregationMap_empty() {
+    assertThat(new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE).getTagValueAggregationMap())
+        .isEmpty();
+  }
+
+  @Test
+  public void testGetStart() {
+    assertThat(new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE).getStart()).isEqualTo(START);
+  }
+
+  @Test
+  public void testRecord() {
+    IntervalBucket bucket = new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE);
+    List<TagValue> tagValues1 = Arrays.<TagValue>asList(TagValue.create("VALUE1"));
+    List<TagValue> tagValues2 = Arrays.<TagValue>asList(TagValue.create("VALUE2"));
+    bucket.record(tagValues1, 5.0, Collections.<String, String>emptyMap(), START);
+    bucket.record(tagValues1, 15.0, Collections.<String, String>emptyMap(), START);
+    bucket.record(tagValues2, 10.0, Collections.<String, String>emptyMap(), START);
+    assertThat(bucket.getTagValueAggregationMap().keySet()).containsExactly(tagValues1, tagValues2);
+    MutableMean mutableMean1 = (MutableMean) bucket.getTagValueAggregationMap().get(tagValues1);
+    MutableMean mutableMean2 = (MutableMean) bucket.getTagValueAggregationMap().get(tagValues2);
+    assertThat(mutableMean1.getSum()).isWithin(TOLERANCE).of(20);
+    assertThat(mutableMean2.getSum()).isWithin(TOLERANCE).of(10);
+    assertThat(mutableMean1.getCount()).isEqualTo(2);
+    assertThat(mutableMean2.getCount()).isEqualTo(1);
+  }
+
+  @Test
+  public void testGetFraction() {
+    Timestamp thirtySecondsAfterStart = Timestamp.create(90, 0);
+    assertThat(
+            new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE)
+                .getFraction(thirtySecondsAfterStart))
+        .isWithin(TOLERANCE)
+        .of(0.5);
+  }
+
+  @Test
+  public void preventCallingGetFractionOnPastBuckets() {
+    IntervalBucket bucket = new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE);
+    Timestamp twoMinutesAfterStart = Timestamp.create(180, 0);
+    thrown.expect(IllegalArgumentException.class);
+    bucket.getFraction(twoMinutesAfterStart);
+  }
+
+  @Test
+  public void preventCallingGetFractionOnFutureBuckets() {
+    IntervalBucket bucket = new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE);
+    Timestamp thirtySecondsBeforeStart = Timestamp.create(30, 0);
+    thrown.expect(IllegalArgumentException.class);
+    bucket.getFraction(thirtySecondsBeforeStart);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureMapInternalTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureMapInternalTest.java
new file mode 100644
index 0000000..19e8a6c
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureMapInternalTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2016-17, 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.Measurement;
+import io.opencensus.stats.Measurement.MeasurementDouble;
+import io.opencensus.stats.Measurement.MeasurementLong;
+import java.util.ArrayList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MeasureMapInternal}. */
+@RunWith(JUnit4.class)
+public class MeasureMapInternalTest {
+
+  @Test
+  public void testPutDouble() {
+    MeasureMapInternal metrics = MeasureMapInternal.builder().put(M1, 44.4).build();
+    assertContains(metrics, MeasurementDouble.create(M1, 44.4));
+  }
+
+  @Test
+  public void testPutLong() {
+    MeasureMapInternal metrics = MeasureMapInternal.builder().put(M3, 9999L).put(M4, 8888L).build();
+    assertContains(metrics, MeasurementLong.create(M3, 9999L), MeasurementLong.create(M4, 8888L));
+  }
+
+  @Test
+  public void testPutAttachment() {
+    MeasureMapInternal metrics =
+        MeasureMapInternal.builder()
+            .putAttachment("k1", "v1")
+            .putAttachment("k2", "v2")
+            .putAttachment("k1", "v3")
+            .build();
+    assertThat(metrics.getAttachments()).containsExactly("k1", "v3", "k2", "v2");
+    assertContains(metrics);
+  }
+
+  @Test
+  public void testCombination() {
+    MeasureMapInternal metrics =
+        MeasureMapInternal.builder()
+            .put(M1, 44.4)
+            .put(M2, 66.6)
+            .put(M3, 9999L)
+            .put(M4, 8888L)
+            .build();
+    assertContains(
+        metrics,
+        MeasurementDouble.create(M1, 44.4),
+        MeasurementDouble.create(M2, 66.6),
+        MeasurementLong.create(M3, 9999L),
+        MeasurementLong.create(M4, 8888L));
+  }
+
+  @Test
+  public void testBuilderEmpty() {
+    MeasureMapInternal metrics = MeasureMapInternal.builder().build();
+    assertContains(metrics);
+  }
+
+  @Test
+  public void testBuilder() {
+    ArrayList<Measurement> expected = new ArrayList<Measurement>(10);
+    MeasureMapInternal.Builder builder = MeasureMapInternal.builder();
+    for (int i = 1; i <= 10; i++) {
+      expected.add(MeasurementDouble.create(makeSimpleMeasureDouble("m" + i), i * 11.1));
+      builder.put(makeSimpleMeasureDouble("m" + i), i * 11.1);
+      assertContains(builder.build(), expected.toArray(new Measurement[i]));
+    }
+  }
+
+  @Test
+  public void testDuplicateMeasureDoubles() {
+    assertContains(
+        MeasureMapInternal.builder().put(M1, 1.0).put(M1, 2.0).build(),
+        MeasurementDouble.create(M1, 2.0));
+    assertContains(
+        MeasureMapInternal.builder().put(M1, 1.0).put(M1, 2.0).put(M1, 3.0).build(),
+        MeasurementDouble.create(M1, 3.0));
+    assertContains(
+        MeasureMapInternal.builder().put(M1, 1.0).put(M2, 2.0).put(M1, 3.0).build(),
+        MeasurementDouble.create(M1, 3.0),
+        MeasurementDouble.create(M2, 2.0));
+    assertContains(
+        MeasureMapInternal.builder().put(M1, 1.0).put(M1, 2.0).put(M2, 2.0).build(),
+        MeasurementDouble.create(M1, 2.0),
+        MeasurementDouble.create(M2, 2.0));
+  }
+
+  @Test
+  public void testDuplicateMeasureLongs() {
+    assertContains(
+        MeasureMapInternal.builder().put(M3, 100L).put(M3, 100L).build(),
+        MeasurementLong.create(M3, 100L));
+    assertContains(
+        MeasureMapInternal.builder().put(M3, 100L).put(M3, 200L).put(M3, 300L).build(),
+        MeasurementLong.create(M3, 300L));
+    assertContains(
+        MeasureMapInternal.builder().put(M3, 100L).put(M4, 200L).put(M3, 300L).build(),
+        MeasurementLong.create(M3, 300L),
+        MeasurementLong.create(M4, 200L));
+    assertContains(
+        MeasureMapInternal.builder().put(M3, 100L).put(M3, 200L).put(M4, 200L).build(),
+        MeasurementLong.create(M3, 200L),
+        MeasurementLong.create(M4, 200L));
+  }
+
+  @Test
+  public void testDuplicateMeasures() {
+    assertContains(
+        MeasureMapInternal.builder().put(M3, 100L).put(M1, 1.0).put(M3, 300L).build(),
+        MeasurementLong.create(M3, 300L),
+        MeasurementDouble.create(M1, 1.0));
+    assertContains(
+        MeasureMapInternal.builder().put(M2, 2.0).put(M3, 100L).put(M2, 3.0).build(),
+        MeasurementDouble.create(M2, 3.0),
+        MeasurementLong.create(M3, 100L));
+  }
+
+  private static final MeasureDouble M1 = makeSimpleMeasureDouble("m1");
+  private static final MeasureDouble M2 = makeSimpleMeasureDouble("m2");
+  private static final MeasureLong M3 = makeSimpleMeasureLong("m3");
+  private static final MeasureLong M4 = makeSimpleMeasureLong("m4");
+
+  private static MeasureDouble makeSimpleMeasureDouble(String measure) {
+    return Measure.MeasureDouble.create(measure, measure + " description", "1");
+  }
+
+  private static MeasureLong makeSimpleMeasureLong(String measure) {
+    return Measure.MeasureLong.create(measure, measure + " description", "1");
+  }
+
+  private static void assertContains(MeasureMapInternal metrics, Measurement... measurements) {
+    assertThat(Lists.newArrayList(metrics.iterator())).containsExactly((Object[]) measurements);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureToViewMapTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureToViewMapTest.java
new file mode 100644
index 0000000..25f33a9
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureToViewMapTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.testing.common.TestClock;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MeasureToViewMap}. */
+@RunWith(JUnit4.class)
+public class MeasureToViewMapTest {
+
+  private static final Measure MEASURE =
+      Measure.MeasureDouble.create("my measurement", "measurement description", "By");
+
+  private static final Name VIEW_NAME = View.Name.create("my view");
+
+  private static final Cumulative CUMULATIVE = Cumulative.create();
+
+  private static final View VIEW =
+      View.create(
+          VIEW_NAME,
+          "view description",
+          MEASURE,
+          Mean.create(),
+          Arrays.asList(TagKey.create("my key")),
+          CUMULATIVE);
+
+  @Test
+  public void testRegisterAndGetView() {
+    MeasureToViewMap measureToViewMap = new MeasureToViewMap();
+    TestClock clock = TestClock.create(Timestamp.create(10, 20));
+    measureToViewMap.registerView(VIEW, clock);
+    clock.setTime(Timestamp.create(30, 40));
+    ViewData viewData = measureToViewMap.getView(VIEW_NAME, clock, State.ENABLED);
+    assertThat(viewData.getView()).isEqualTo(VIEW);
+    assertThat(viewData.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(10, 20), Timestamp.create(30, 40)));
+    assertThat(viewData.getAggregationMap()).isEmpty();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/MetricUtilsTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MetricUtilsTest.java
new file mode 100644
index 0000000..66e971f
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MetricUtilsTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2018, 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.View.Name;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MetricUtils}. */
+@RunWith(JUnit4.class)
+public class MetricUtilsTest {
+
+  private static final TagKey KEY = TagKey.create("KEY");
+  private static final TagValue VALUE = TagValue.create("VALUE");
+  private static final TagValue VALUE_2 = TagValue.create("VALUE_2");
+  private static final String MEASURE_NAME = "my measurement";
+  private static final String MEASURE_NAME_2 = "my measurement 2";
+  private static final String MEASURE_UNIT = "us";
+  private static final String MEASURE_DESCRIPTION = "measure description";
+  private static final MeasureDouble MEASURE_DOUBLE =
+      MeasureDouble.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT);
+  private static final MeasureLong MEASURE_LONG =
+      MeasureLong.create(MEASURE_NAME_2, MEASURE_DESCRIPTION, MEASURE_UNIT);
+  private static final Name VIEW_NAME = Name.create("my view");
+  private static final Name VIEW_NAME_2 = Name.create("my view 2");
+  private static final String VIEW_DESCRIPTION = "view description";
+  private static final Duration TEN_SECONDS = Duration.create(10, 0);
+  private static final Interval INTERVAL = Interval.create(TEN_SECONDS);
+  private static final BucketBoundaries BUCKET_BOUNDARIES =
+      BucketBoundaries.create(Arrays.asList(-10.0, 0.0, 10.0));
+  private static final Sum SUM = Sum.create();
+  private static final Count COUNT = Count.create();
+  private static final Mean MEAN = Mean.create();
+  private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+  private static final LastValue LAST_VALUE = LastValue.create();
+  private static final View VIEW_1 =
+      View.create(
+          VIEW_NAME, VIEW_DESCRIPTION, MEASURE_DOUBLE, LAST_VALUE, Collections.singletonList(KEY));
+  private static final View VIEW_2 =
+      View.create(
+          VIEW_NAME_2,
+          VIEW_DESCRIPTION,
+          MEASURE_DOUBLE,
+          MEAN,
+          Collections.singletonList(KEY),
+          INTERVAL);
+  private static final Timestamp TIMESTAMP = Timestamp.fromMillis(1000);
+
+  @Test
+  public void viewToMetricDescriptor() {
+    MetricDescriptor metricDescriptor = MetricUtils.viewToMetricDescriptor(VIEW_1);
+    assertThat(metricDescriptor).isNotNull();
+    assertThat(metricDescriptor.getName()).isEqualTo(VIEW_NAME.asString());
+    assertThat(metricDescriptor.getUnit()).isEqualTo(MEASURE_UNIT);
+    assertThat(metricDescriptor.getType()).isEqualTo(Type.GAUGE_DOUBLE);
+    assertThat(metricDescriptor.getDescription()).isEqualTo(VIEW_DESCRIPTION);
+    assertThat(metricDescriptor.getLabelKeys()).containsExactly(LabelKey.create(KEY.getName(), ""));
+  }
+
+  @Test
+  public void viewToMetricDescriptor_NoIntervalViews() {
+    MetricDescriptor metricDescriptor = MetricUtils.viewToMetricDescriptor(VIEW_2);
+    assertThat(metricDescriptor).isNull();
+  }
+
+  @Test
+  public void getType() {
+    assertThat(MetricUtils.getType(MEASURE_DOUBLE, LAST_VALUE)).isEqualTo(Type.GAUGE_DOUBLE);
+    assertThat(MetricUtils.getType(MEASURE_LONG, LAST_VALUE)).isEqualTo(Type.GAUGE_INT64);
+    assertThat(MetricUtils.getType(MEASURE_DOUBLE, SUM)).isEqualTo(Type.CUMULATIVE_DOUBLE);
+    assertThat(MetricUtils.getType(MEASURE_LONG, SUM)).isEqualTo(Type.CUMULATIVE_INT64);
+    assertThat(MetricUtils.getType(MEASURE_DOUBLE, MEAN)).isEqualTo(Type.CUMULATIVE_DOUBLE);
+    assertThat(MetricUtils.getType(MEASURE_LONG, MEAN)).isEqualTo(Type.CUMULATIVE_DOUBLE);
+    assertThat(MetricUtils.getType(MEASURE_DOUBLE, COUNT)).isEqualTo(Type.CUMULATIVE_INT64);
+    assertThat(MetricUtils.getType(MEASURE_LONG, COUNT)).isEqualTo(Type.CUMULATIVE_INT64);
+    assertThat(MetricUtils.getType(MEASURE_DOUBLE, DISTRIBUTION))
+        .isEqualTo(Type.CUMULATIVE_DISTRIBUTION);
+    assertThat(MetricUtils.getType(MEASURE_LONG, DISTRIBUTION))
+        .isEqualTo(Type.CUMULATIVE_DISTRIBUTION);
+  }
+
+  @Test
+  public void tagValuesToLabelValues() {
+    List<TagValue> tagValues = Arrays.asList(VALUE, VALUE_2, null);
+    assertThat(MetricUtils.tagValuesToLabelValues(tagValues))
+        .containsExactly(
+            LabelValue.create(VALUE.asString()),
+            LabelValue.create(VALUE_2.asString()),
+            LabelValue.create(null));
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/MutableAggregationTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableAggregationTest.java
new file mode 100644
index 0000000..a6139e5
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableAggregationTest.java
@@ -0,0 +1,339 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.stats.StatsTestUtil.assertAggregationDataEquals;
+
+import com.google.common.collect.ImmutableList;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.stats.MutableAggregation.MutableCount;
+import io.opencensus.implcore.stats.MutableAggregation.MutableDistribution;
+import io.opencensus.implcore.stats.MutableAggregation.MutableLastValueDouble;
+import io.opencensus.implcore.stats.MutableAggregation.MutableLastValueLong;
+import io.opencensus.implcore.stats.MutableAggregation.MutableMean;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSumDouble;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSumLong;
+import io.opencensus.metrics.export.Distribution;
+import io.opencensus.metrics.export.Distribution.Bucket;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link io.opencensus.implcore.stats.MutableAggregation}. */
+@RunWith(JUnit4.class)
+public class MutableAggregationTest {
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private static final double TOLERANCE = 1e-6;
+  private static final BucketBoundaries BUCKET_BOUNDARIES =
+      BucketBoundaries.create(Arrays.asList(-10.0, 0.0, 10.0));
+  private static final BucketBoundaries BUCKET_BOUNDARIES_EMPTY =
+      BucketBoundaries.create(Collections.<Double>emptyList());
+  private static final Timestamp TIMESTAMP = Timestamp.create(60, 0);
+
+  @Test
+  public void testCreateEmpty() {
+    assertThat(MutableSumDouble.create().getSum()).isWithin(TOLERANCE).of(0);
+    assertThat(MutableSumLong.create().getSum()).isWithin(TOLERANCE).of(0);
+    assertThat(MutableCount.create().getCount()).isEqualTo(0);
+    assertThat(MutableMean.create().getMean()).isWithin(TOLERANCE).of(0);
+    assertThat(MutableLastValueDouble.create().getLastValue()).isNaN();
+    assertThat(MutableLastValueLong.create().getLastValue()).isNaN();
+
+    BucketBoundaries bucketBoundaries = BucketBoundaries.create(Arrays.asList(0.1, 2.2, 33.3));
+    MutableDistribution mutableDistribution = MutableDistribution.create(bucketBoundaries);
+    assertThat(mutableDistribution.getMean()).isWithin(TOLERANCE).of(0);
+    assertThat(mutableDistribution.getCount()).isEqualTo(0);
+    assertThat(mutableDistribution.getMin()).isPositiveInfinity();
+    assertThat(mutableDistribution.getMax()).isNegativeInfinity();
+    assertThat(mutableDistribution.getSumOfSquaredDeviations()).isWithin(TOLERANCE).of(0);
+    assertThat(mutableDistribution.getBucketCounts()).isEqualTo(new long[4]);
+    assertThat(mutableDistribution.getExemplars()).isEqualTo(new Exemplar[4]);
+
+    MutableDistribution mutableDistributionNoHistogram =
+        MutableDistribution.create(BUCKET_BOUNDARIES_EMPTY);
+    assertThat(mutableDistributionNoHistogram.getExemplars()).isNull();
+  }
+
+  @Test
+  public void testNullBucketBoundaries() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("bucketBoundaries should not be null.");
+    MutableDistribution.create(null);
+  }
+
+  @Test
+  public void testNoBoundaries() {
+    List<Double> buckets = Arrays.asList();
+    MutableDistribution noBoundaries = MutableDistribution.create(BucketBoundaries.create(buckets));
+    assertThat(noBoundaries.getBucketCounts().length).isEqualTo(1);
+    assertThat(noBoundaries.getBucketCounts()[0]).isEqualTo(0);
+  }
+
+  @Test
+  public void testAdd() {
+    List<MutableAggregation> aggregations =
+        Arrays.asList(
+            MutableSumDouble.create(),
+            MutableSumLong.create(),
+            MutableCount.create(),
+            MutableMean.create(),
+            MutableDistribution.create(BUCKET_BOUNDARIES),
+            MutableLastValueDouble.create(),
+            MutableLastValueLong.create());
+
+    List<Double> values = Arrays.asList(-1.0, 1.0, -5.0, 20.0, 5.0);
+
+    for (double value : values) {
+      for (MutableAggregation aggregation : aggregations) {
+        aggregation.add(value, Collections.<String, String>emptyMap(), TIMESTAMP);
+      }
+    }
+
+    assertAggregationDataEquals(
+        aggregations.get(0).toAggregationData(),
+        AggregationData.SumDataDouble.create(20.0),
+        TOLERANCE);
+    assertAggregationDataEquals(
+        aggregations.get(1).toAggregationData(), AggregationData.SumDataLong.create(20), TOLERANCE);
+    assertAggregationDataEquals(
+        aggregations.get(2).toAggregationData(), AggregationData.CountData.create(5), TOLERANCE);
+    assertAggregationDataEquals(
+        aggregations.get(3).toAggregationData(),
+        AggregationData.MeanData.create(4.0, 5),
+        TOLERANCE);
+    assertAggregationDataEquals(
+        aggregations.get(4).toAggregationData(),
+        AggregationData.DistributionData.create(
+            4.0, 5, -5.0, 20.0, 372, Arrays.asList(0L, 2L, 2L, 1L)),
+        TOLERANCE);
+    assertAggregationDataEquals(
+        aggregations.get(5).toAggregationData(),
+        AggregationData.LastValueDataDouble.create(5.0),
+        TOLERANCE);
+    assertAggregationDataEquals(
+        aggregations.get(6).toAggregationData(),
+        AggregationData.LastValueDataLong.create(5),
+        TOLERANCE);
+  }
+
+  @Test
+  public void testAdd_DistributionWithExemplarAttachments() {
+    MutableDistribution mutableDistribution = MutableDistribution.create(BUCKET_BOUNDARIES);
+    MutableDistribution mutableDistributionNoHistogram =
+        MutableDistribution.create(BUCKET_BOUNDARIES_EMPTY);
+    List<Double> values = Arrays.asList(-1.0, 1.0, -5.0, 20.0, 5.0);
+    List<Map<String, String>> attachmentsList =
+        ImmutableList.<Map<String, String>>of(
+            Collections.<String, String>singletonMap("k1", "v1"),
+            Collections.<String, String>singletonMap("k2", "v2"),
+            Collections.<String, String>singletonMap("k3", "v3"),
+            Collections.<String, String>singletonMap("k4", "v4"),
+            Collections.<String, String>singletonMap("k5", "v5"));
+    List<Timestamp> timestamps =
+        Arrays.asList(
+            Timestamp.fromMillis(500),
+            Timestamp.fromMillis(1000),
+            Timestamp.fromMillis(2000),
+            Timestamp.fromMillis(3000),
+            Timestamp.fromMillis(4000));
+    for (int i = 0; i < values.size(); i++) {
+      mutableDistribution.add(values.get(i), attachmentsList.get(i), timestamps.get(i));
+      mutableDistributionNoHistogram.add(values.get(i), attachmentsList.get(i), timestamps.get(i));
+    }
+
+    // Each bucket can only have up to one exemplar. If there are more than one exemplars in a
+    // bucket, only the last one will be kept.
+    List<Exemplar> expected =
+        Arrays.<Exemplar>asList(
+            null,
+            Exemplar.create(values.get(2), timestamps.get(2), attachmentsList.get(2)),
+            Exemplar.create(values.get(4), timestamps.get(4), attachmentsList.get(4)),
+            Exemplar.create(values.get(3), timestamps.get(3), attachmentsList.get(3)));
+    assertThat(mutableDistribution.getExemplars())
+        .asList()
+        .containsExactlyElementsIn(expected)
+        .inOrder();
+    assertThat(mutableDistributionNoHistogram.getExemplars()).isNull();
+  }
+
+  @Test
+  public void testCombine_SumCountMean() {
+    // combine() for Mutable Sum, Count and Mean will pick up fractional stats
+    List<MutableAggregation> aggregations1 =
+        Arrays.asList(
+            MutableSumDouble.create(),
+            MutableSumLong.create(),
+            MutableCount.create(),
+            MutableMean.create());
+    List<MutableAggregation> aggregations2 =
+        Arrays.asList(
+            MutableSumDouble.create(),
+            MutableSumLong.create(),
+            MutableCount.create(),
+            MutableMean.create());
+
+    for (double val : Arrays.asList(-1.0, -5.0)) {
+      for (MutableAggregation aggregation : aggregations1) {
+        aggregation.add(val, Collections.<String, String>emptyMap(), TIMESTAMP);
+      }
+    }
+    for (double val : Arrays.asList(10.0, 50.0)) {
+      for (MutableAggregation aggregation : aggregations2) {
+        aggregation.add(val, Collections.<String, String>emptyMap(), TIMESTAMP);
+      }
+    }
+
+    List<MutableAggregation> combined =
+        Arrays.asList(
+            MutableSumDouble.create(),
+            MutableSumLong.create(),
+            MutableCount.create(),
+            MutableMean.create());
+    double fraction1 = 1.0;
+    double fraction2 = 0.6;
+    for (int i = 0; i < combined.size(); i++) {
+      combined.get(i).combine(aggregations1.get(i), fraction1);
+      combined.get(i).combine(aggregations2.get(i), fraction2);
+    }
+
+    assertThat(((MutableSumDouble) combined.get(0)).getSum()).isWithin(TOLERANCE).of(30);
+    assertThat(((MutableSumLong) combined.get(1)).getSum()).isWithin(TOLERANCE).of(30);
+    assertThat(((MutableCount) combined.get(2)).getCount()).isEqualTo(3);
+    assertThat(((MutableMean) combined.get(3)).getMean()).isWithin(TOLERANCE).of(10);
+  }
+
+  @Test
+  public void testCombine_Distribution() {
+    // combine() for Mutable Distribution will ignore fractional stats
+    MutableDistribution distribution1 = MutableDistribution.create(BUCKET_BOUNDARIES);
+    MutableDistribution distribution2 = MutableDistribution.create(BUCKET_BOUNDARIES);
+    MutableDistribution distribution3 = MutableDistribution.create(BUCKET_BOUNDARIES);
+
+    for (double val : Arrays.asList(5.0, -5.0)) {
+      distribution1.add(val, Collections.<String, String>emptyMap(), TIMESTAMP);
+    }
+    for (double val : Arrays.asList(10.0, 20.0)) {
+      distribution2.add(val, Collections.<String, String>emptyMap(), TIMESTAMP);
+    }
+    for (double val : Arrays.asList(-10.0, 15.0, -15.0, -20.0)) {
+      distribution3.add(val, Collections.<String, String>emptyMap(), TIMESTAMP);
+    }
+
+    MutableDistribution combined = MutableDistribution.create(BUCKET_BOUNDARIES);
+    combined.combine(distribution1, 1.0); // distribution1 will be combined
+    combined.combine(distribution2, 0.6); // distribution2 will be ignored
+    verifyMutableDistribution(combined, 0, 2, -5, 5, 50.0, new long[] {0, 1, 1, 0}, TOLERANCE);
+
+    combined.combine(distribution2, 1.0); // distribution2 will be combined
+    verifyMutableDistribution(combined, 7.5, 4, -5, 20, 325.0, new long[] {0, 1, 1, 2}, TOLERANCE);
+
+    combined.combine(distribution3, 1.0); // distribution3 will be combined
+    verifyMutableDistribution(combined, 0, 8, -20, 20, 1500.0, new long[] {2, 2, 1, 3}, TOLERANCE);
+  }
+
+  @Test
+  public void mutableAggregation_ToAggregationData() {
+    assertThat(MutableSumDouble.create().toAggregationData()).isEqualTo(SumDataDouble.create(0));
+    assertThat(MutableSumLong.create().toAggregationData()).isEqualTo(SumDataLong.create(0));
+    assertThat(MutableCount.create().toAggregationData()).isEqualTo(CountData.create(0));
+    assertThat(MutableMean.create().toAggregationData()).isEqualTo(MeanData.create(0, 0));
+    assertThat(MutableDistribution.create(BUCKET_BOUNDARIES).toAggregationData())
+        .isEqualTo(
+            DistributionData.create(
+                0,
+                0,
+                Double.POSITIVE_INFINITY,
+                Double.NEGATIVE_INFINITY,
+                0,
+                Arrays.asList(0L, 0L, 0L, 0L)));
+    assertThat(MutableLastValueDouble.create().toAggregationData())
+        .isEqualTo(LastValueDataDouble.create(Double.NaN));
+    assertThat(MutableLastValueLong.create().toAggregationData())
+        .isEqualTo(LastValueDataLong.create(0));
+  }
+
+  @Test
+  public void mutableAggregation_ToPoint() {
+    assertThat(MutableSumDouble.create().toPoint(TIMESTAMP))
+        .isEqualTo(Point.create(Value.doubleValue(0), TIMESTAMP));
+    assertThat(MutableSumLong.create().toPoint(TIMESTAMP))
+        .isEqualTo(Point.create(Value.longValue(0), TIMESTAMP));
+    assertThat(MutableCount.create().toPoint(TIMESTAMP))
+        .isEqualTo(Point.create(Value.longValue(0), TIMESTAMP));
+    assertThat(MutableMean.create().toPoint(TIMESTAMP))
+        .isEqualTo(Point.create(Value.doubleValue(0), TIMESTAMP));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("bucket boundary should be > 0");
+    assertThat(MutableDistribution.create(BUCKET_BOUNDARIES).toPoint(TIMESTAMP))
+        .isEqualTo(
+            Point.create(
+                Value.distributionValue(
+                    Distribution.create(
+                        0,
+                        0,
+                        0,
+                        BucketOptions.explicitOptions(BUCKET_BOUNDARIES.getBoundaries()),
+                        Arrays.asList(
+                            Bucket.create(0),
+                            Bucket.create(0),
+                            Bucket.create(0),
+                            Bucket.create(0)))),
+                TIMESTAMP));
+  }
+
+  private static void verifyMutableDistribution(
+      MutableDistribution mutableDistribution,
+      double mean,
+      long count,
+      double min,
+      double max,
+      double sumOfSquaredDeviations,
+      long[] bucketCounts,
+      double tolerance) {
+    assertThat(mutableDistribution.getMean()).isWithin(tolerance).of(mean);
+    assertThat(mutableDistribution.getCount()).isEqualTo(count);
+    assertThat(mutableDistribution.getMin()).isWithin(tolerance).of(min);
+    assertThat(mutableDistribution.getMax()).isWithin(tolerance).of(max);
+    assertThat(mutableDistribution.getSumOfSquaredDeviations())
+        .isWithin(tolerance)
+        .of(sumOfSquaredDeviations);
+    assertThat(mutableDistribution.getBucketCounts()).isEqualTo(bucketCounts);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/MutableViewDataTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableViewDataTest.java
new file mode 100644
index 0000000..06f50fe
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableViewDataTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MutableViewData}. */
+@RunWith(JUnit4.class)
+public class MutableViewDataTest {
+
+  @Test
+  public void testConstants() {
+    assertThat(MutableViewData.ZERO_TIMESTAMP).isEqualTo(Timestamp.create(0, 0));
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/RecordUtilsTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/RecordUtilsTest.java
new file mode 100644
index 0000000..1e22a7a
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/RecordUtilsTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2018, 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import io.opencensus.implcore.stats.MutableAggregation.MutableDistribution;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link RecordUtils}. */
+@RunWith(JUnit4.class)
+public class RecordUtilsTest {
+
+  private static final double EPSILON = 1e-7;
+  private static final MeasureDouble MEASURE_DOUBLE =
+      MeasureDouble.create("measure1", "description", "1");
+  private static final MeasureLong MEASURE_LONG =
+      MeasureLong.create("measure2", "description", "1");
+  private static final TagKey ORIGINATOR = TagKey.create("originator");
+  private static final TagKey CALLER = TagKey.create("caller");
+  private static final TagKey METHOD = TagKey.create("method");
+  private static final TagValue CALLER_V = TagValue.create("some caller");
+  private static final TagValue METHOD_V = TagValue.create("some method");
+
+  @Test
+  public void testConstants() {
+    assertThat(RecordUtils.UNKNOWN_TAG_VALUE).isNull();
+  }
+
+  @Test
+  public void testGetTagValues() {
+    List<TagKey> columns = Arrays.asList(CALLER, METHOD, ORIGINATOR);
+    Map<TagKey, TagValue> tags = ImmutableMap.of(CALLER, CALLER_V, METHOD, METHOD_V);
+
+    assertThat(RecordUtils.getTagValues(tags, columns))
+        .containsExactly(CALLER_V, METHOD_V, RecordUtils.UNKNOWN_TAG_VALUE)
+        .inOrder();
+  }
+
+  @Test
+  public void createMutableAggregation() {
+    BucketBoundaries bucketBoundaries = BucketBoundaries.create(Arrays.asList(-1.0, 0.0, 1.0));
+
+    assertThat(
+            RecordUtils.createMutableAggregation(Sum.create(), MEASURE_DOUBLE).toAggregationData())
+        .isEqualTo(SumDataDouble.create(0));
+    assertThat(RecordUtils.createMutableAggregation(Sum.create(), MEASURE_LONG).toAggregationData())
+        .isEqualTo(SumDataLong.create(0));
+    assertThat(
+            RecordUtils.createMutableAggregation(Count.create(), MEASURE_DOUBLE)
+                .toAggregationData())
+        .isEqualTo(CountData.create(0));
+    assertThat(
+            RecordUtils.createMutableAggregation(Count.create(), MEASURE_LONG).toAggregationData())
+        .isEqualTo(CountData.create(0));
+    assertThat(
+            RecordUtils.createMutableAggregation(Mean.create(), MEASURE_DOUBLE).toAggregationData())
+        .isEqualTo(MeanData.create(0, 0));
+    assertThat(
+            RecordUtils.createMutableAggregation(Mean.create(), MEASURE_LONG).toAggregationData())
+        .isEqualTo(MeanData.create(0, 0));
+    assertThat(
+            RecordUtils.createMutableAggregation(LastValue.create(), MEASURE_DOUBLE)
+                .toAggregationData())
+        .isEqualTo(LastValueDataDouble.create(Double.NaN));
+    assertThat(
+            RecordUtils.createMutableAggregation(LastValue.create(), MEASURE_LONG)
+                .toAggregationData())
+        .isEqualTo(LastValueDataLong.create(0));
+
+    MutableDistribution mutableDistribution =
+        (MutableDistribution)
+            RecordUtils.createMutableAggregation(
+                Distribution.create(bucketBoundaries), MEASURE_DOUBLE);
+    assertThat(mutableDistribution.getMin()).isPositiveInfinity();
+    assertThat(mutableDistribution.getMax()).isNegativeInfinity();
+    assertThat(mutableDistribution.getSumOfSquaredDeviations()).isWithin(EPSILON).of(0);
+    assertThat(mutableDistribution.getBucketCounts()).isEqualTo(new long[4]);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/StatsComponentImplBaseTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsComponentImplBaseTest.java
new file mode 100644
index 0000000..04861df
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsComponentImplBaseTest.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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.StatsComponent;
+import io.opencensus.testing.common.TestClock;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link StatsComponentImplBase}. */
+@RunWith(JUnit4.class)
+public final class StatsComponentImplBaseTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private final StatsComponent statsComponent =
+      new StatsComponentImplBase(new SimpleEventQueue(), TestClock.create());
+
+  @Test
+  public void defaultState() {
+    assertThat(statsComponent.getState()).isEqualTo(StatsCollectionState.ENABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void setState_Disabled() {
+    statsComponent.setState(StatsCollectionState.DISABLED);
+    assertThat(statsComponent.getState()).isEqualTo(StatsCollectionState.DISABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void setState_Enabled() {
+    statsComponent.setState(StatsCollectionState.DISABLED);
+    statsComponent.setState(StatsCollectionState.ENABLED);
+    assertThat(statsComponent.getState()).isEqualTo(StatsCollectionState.ENABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void setState_DisallowsNull() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("newState");
+    statsComponent.setState(null);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void preventSettingStateAfterGettingState() {
+    statsComponent.setState(StatsCollectionState.DISABLED);
+    statsComponent.getState();
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("State was already read, cannot set state.");
+    statsComponent.setState(StatsCollectionState.ENABLED);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/StatsRecorderImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsRecorderImplTest.java
new file mode 100644
index 0000000..bd8b5b8
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsRecorderImplTest.java
@@ -0,0 +1,349 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.stats.MutableViewData.ZERO_TIMESTAMP;
+import static io.opencensus.implcore.stats.StatsTestUtil.createEmptyViewData;
+
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Context;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.stats.StatsTestUtil.SimpleTagContext;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.StatsComponent;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.unsafe.ContextUtils;
+import io.opencensus.testing.common.TestClock;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link StatsRecorderImpl}. */
+@RunWith(JUnit4.class)
+public final class StatsRecorderImplTest {
+  private static final TagKey KEY = TagKey.create("KEY");
+  private static final TagValue VALUE = TagValue.create("VALUE");
+  private static final TagValue VALUE_2 = TagValue.create("VALUE_2");
+  private static final MeasureDouble MEASURE_DOUBLE =
+      MeasureDouble.create("my measurement", "description", "us");
+  private static final MeasureDouble MEASURE_DOUBLE_NO_VIEW_1 =
+      MeasureDouble.create("my measurement no view 1", "description", "us");
+  private static final MeasureDouble MEASURE_DOUBLE_NO_VIEW_2 =
+      MeasureDouble.create("my measurement no view 2", "description", "us");
+  private static final View.Name VIEW_NAME = View.Name.create("my view");
+  private static final BucketBoundaries BUCKET_BOUNDARIES =
+      BucketBoundaries.create(Arrays.asList(-10.0, 0.0, 10.0));
+  private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+  private static final Distribution DISTRIBUTION_NO_HISTOGRAM =
+      Distribution.create(BucketBoundaries.create(Collections.<Double>emptyList()));
+  private static final Timestamp START_TIME = Timestamp.fromMillis(0);
+  private static final Duration ONE_SECOND = Duration.fromMillis(1000);
+
+  private final TestClock testClock = TestClock.create();
+  private final StatsComponent statsComponent =
+      new StatsComponentImplBase(new SimpleEventQueue(), testClock);
+
+  private final ViewManager viewManager = statsComponent.getViewManager();
+  private final StatsRecorder statsRecorder = statsComponent.getStatsRecorder();
+
+  @Test
+  public void record_CurrentContextNotSet() {
+    View view =
+        View.create(
+            VIEW_NAME,
+            "description",
+            MEASURE_DOUBLE,
+            Sum.create(),
+            Arrays.asList(KEY),
+            Cumulative.create());
+    viewManager.registerView(view);
+    statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 1.0).record();
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+
+    // record() should have used the default TagContext, so the tag value should be null.
+    assertThat(viewData.getAggregationMap().keySet())
+        .containsExactly(Arrays.asList((TagValue) null));
+  }
+
+  @Test
+  public void record_CurrentContextSet() {
+    View view =
+        View.create(
+            VIEW_NAME,
+            "description",
+            MEASURE_DOUBLE,
+            Sum.create(),
+            Arrays.asList(KEY),
+            Cumulative.create());
+    viewManager.registerView(view);
+    Context orig =
+        Context.current()
+            .withValue(ContextUtils.TAG_CONTEXT_KEY, new SimpleTagContext(Tag.create(KEY, VALUE)))
+            .attach();
+    try {
+      statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 1.0).record();
+    } finally {
+      Context.current().detach(orig);
+    }
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+
+    // record() should have used the given TagContext.
+    assertThat(viewData.getAggregationMap().keySet()).containsExactly(Arrays.asList(VALUE));
+  }
+
+  @Test
+  public void record_UnregisteredMeasure() {
+    View view =
+        View.create(
+            VIEW_NAME,
+            "description",
+            MEASURE_DOUBLE,
+            Sum.create(),
+            Arrays.asList(KEY),
+            Cumulative.create());
+    viewManager.registerView(view);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE_NO_VIEW_1, 1.0)
+        .put(MEASURE_DOUBLE, 2.0)
+        .put(MEASURE_DOUBLE_NO_VIEW_2, 3.0)
+        .record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+
+    // There should be one entry.
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(Sum.create(), MEASURE_DOUBLE, 2.0)),
+        1e-6);
+  }
+
+  @Test
+  public void record_WithAttachments_Distribution() {
+    testClock.setTime(START_TIME);
+    View view =
+        View.create(VIEW_NAME, "description", MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+    viewManager.registerView(view);
+    recordWithAttachments();
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertThat(viewData).isNotNull();
+    DistributionData distributionData =
+        (DistributionData) viewData.getAggregationMap().get(Collections.singletonList(VALUE));
+    List<Exemplar> expected =
+        Arrays.asList(
+            Exemplar.create(-20.0, Timestamp.create(4, 0), Collections.singletonMap("k3", "v1")),
+            Exemplar.create(-5.0, Timestamp.create(5, 0), Collections.singletonMap("k3", "v3")),
+            Exemplar.create(1.0, Timestamp.create(2, 0), Collections.singletonMap("k2", "v2")),
+            Exemplar.create(12.0, Timestamp.create(3, 0), Collections.singletonMap("k1", "v3")));
+    assertThat(distributionData.getExemplars()).containsExactlyElementsIn(expected).inOrder();
+  }
+
+  @Test
+  public void record_WithAttachments_DistributionNoHistogram() {
+    testClock.setTime(START_TIME);
+    View view =
+        View.create(
+            VIEW_NAME,
+            "description",
+            MEASURE_DOUBLE,
+            DISTRIBUTION_NO_HISTOGRAM,
+            Arrays.asList(KEY));
+    viewManager.registerView(view);
+    recordWithAttachments();
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertThat(viewData).isNotNull();
+    DistributionData distributionData =
+        (DistributionData) viewData.getAggregationMap().get(Collections.singletonList(VALUE));
+    // Recording exemplar has no effect if there's no histogram.
+    assertThat(distributionData.getExemplars()).isEmpty();
+  }
+
+  @Test
+  public void record_WithAttachments_Count() {
+    testClock.setTime(START_TIME);
+    View view =
+        View.create(VIEW_NAME, "description", MEASURE_DOUBLE, Count.create(), Arrays.asList(KEY));
+    viewManager.registerView(view);
+    recordWithAttachments();
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertThat(viewData).isNotNull();
+    CountData countData =
+        (CountData) viewData.getAggregationMap().get(Collections.singletonList(VALUE));
+    // Recording exemplar does not affect views with an aggregation other than distribution.
+    assertThat(countData.getCount()).isEqualTo(6L);
+  }
+
+  private void recordWithAttachments() {
+    TagContext context = new SimpleTagContext(Tag.create(KEY, VALUE));
+
+    // The test Distribution has bucket boundaries [-10.0, 0.0, 10.0].
+
+    testClock.advanceTime(ONE_SECOND); // 1st second.
+    // -1.0 is in the 2nd bucket [-10.0, 0.0).
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, -1.0)
+        .putAttachment("k1", "v1")
+        .record(context);
+
+    testClock.advanceTime(ONE_SECOND); // 2nd second.
+    // 1.0 is in the 3rd bucket [0.0, 10.0).
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.0)
+        .putAttachment("k2", "v2")
+        .record(context);
+
+    testClock.advanceTime(ONE_SECOND); // 3rd second.
+    // 12.0 is in the 4th bucket [10.0, +Inf).
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 12.0)
+        .putAttachment("k1", "v3")
+        .record(context);
+
+    testClock.advanceTime(ONE_SECOND); // 4th second.
+    // -20.0 is in the 1st bucket [-Inf, -10.0).
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, -20.0)
+        .putAttachment("k3", "v1")
+        .record(context);
+
+    testClock.advanceTime(ONE_SECOND); // 5th second.
+    // -5.0 is in the 2nd bucket [-10.0, 0), should overwrite the previous exemplar -1.0.
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, -5.0)
+        .putAttachment("k3", "v3")
+        .record(context);
+
+    testClock.advanceTime(ONE_SECOND); // 6th second.
+    // -3.0 is in the 2nd bucket [-10.0, 0), but this value doesn't come with attachments, so it
+    // shouldn't overwrite the previous exemplar (-5.0).
+    statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, -3.0).record(context);
+  }
+
+  @Test
+  public void recordTwice() {
+    View view =
+        View.create(
+            VIEW_NAME,
+            "description",
+            MEASURE_DOUBLE,
+            Sum.create(),
+            Arrays.asList(KEY),
+            Cumulative.create());
+    viewManager.registerView(view);
+    MeasureMap statsRecord = statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 1.0);
+    statsRecord.record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+    statsRecord.record(new SimpleTagContext(Tag.create(KEY, VALUE_2)));
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+
+    // There should be two entries.
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(Sum.create(), MEASURE_DOUBLE, 1.0),
+            Arrays.asList(VALUE_2),
+            StatsTestUtil.createAggregationData(Sum.create(), MEASURE_DOUBLE, 1.0)),
+        1e-6);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void record_StatsDisabled() {
+    View view =
+        View.create(
+            VIEW_NAME,
+            "description",
+            MEASURE_DOUBLE,
+            Sum.create(),
+            Arrays.asList(KEY),
+            Cumulative.create());
+
+    viewManager.registerView(view);
+    statsComponent.setState(StatsCollectionState.DISABLED);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.0)
+        .record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+    assertThat(viewManager.getView(VIEW_NAME)).isEqualTo(createEmptyViewData(view));
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void record_StatsReenabled() {
+    View view =
+        View.create(
+            VIEW_NAME,
+            "description",
+            MEASURE_DOUBLE,
+            Sum.create(),
+            Arrays.asList(KEY),
+            Cumulative.create());
+    viewManager.registerView(view);
+
+    statsComponent.setState(StatsCollectionState.DISABLED);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.0)
+        .record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+    assertThat(viewManager.getView(VIEW_NAME)).isEqualTo(createEmptyViewData(view));
+
+    statsComponent.setState(StatsCollectionState.ENABLED);
+    assertThat(viewManager.getView(VIEW_NAME).getAggregationMap()).isEmpty();
+    assertThat(viewManager.getView(VIEW_NAME).getWindowData())
+        .isNotEqualTo(CumulativeData.create(ZERO_TIMESTAMP, ZERO_TIMESTAMP));
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 4.0)
+        .record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewManager.getView(VIEW_NAME).getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(Sum.create(), MEASURE_DOUBLE, 4.0)),
+        1e-6);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/StatsTestUtil.java b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsTestUtil.java
new file mode 100644
index 0000000..ea1bf34
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsTestUtil.java
@@ -0,0 +1,232 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.stats.MutableViewData.ZERO_TIMESTAMP;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+
+/** Stats test utilities. */
+final class StatsTestUtil {
+
+  private static final Timestamp EMPTY = Timestamp.create(0, 0);
+
+  private StatsTestUtil() {}
+
+  /**
+   * Creates an {@link AggregationData} by adding the given sequence of values, based on the
+   * definition of the given {@link Aggregation}.
+   *
+   * @param aggregation the {@code Aggregation} to apply the values to.
+   * @param values the values to add to the {@code MutableAggregation}s.
+   * @return an {@code AggregationData}.
+   */
+  static AggregationData createAggregationData(
+      Aggregation aggregation, Measure measure, double... values) {
+    MutableAggregation mutableAggregation =
+        RecordUtils.createMutableAggregation(aggregation, measure);
+    for (double value : values) {
+      mutableAggregation.add(value, Collections.<String, String>emptyMap(), EMPTY);
+    }
+    return mutableAggregation.toAggregationData();
+  }
+
+  /**
+   * Compare the actual and expected AggregationMap within the given tolerance.
+   *
+   * @param expected the expected map.
+   * @param actual the actual mapping from {@code List<TagValue>} to {@code AggregationData}.
+   * @param tolerance the tolerance used for {@code double} comparison.
+   */
+  static void assertAggregationMapEquals(
+      Map<? extends List<? extends TagValue>, ? extends AggregationData> actual,
+      Map<? extends List<? extends TagValue>, ? extends AggregationData> expected,
+      double tolerance) {
+    assertThat(actual.keySet()).containsExactlyElementsIn(expected.keySet());
+    for (Entry<? extends List<? extends TagValue>, ? extends AggregationData> entry :
+        actual.entrySet()) {
+      assertAggregationDataEquals(expected.get(entry.getKey()), entry.getValue(), tolerance);
+    }
+  }
+
+  /**
+   * Compare the expected and actual {@code AggregationData} within the given tolerance.
+   *
+   * @param expected the expected {@code AggregationData}.
+   * @param actual the actual {@code AggregationData}.
+   * @param tolerance the tolerance used for {@code double} comparison.
+   */
+  static void assertAggregationDataEquals(
+      AggregationData expected, final AggregationData actual, final double tolerance) {
+    expected.match(
+        new Function<SumDataDouble, Void>() {
+          @Override
+          public Void apply(SumDataDouble arg) {
+            assertThat(actual).isInstanceOf(SumDataDouble.class);
+            assertThat(((SumDataDouble) actual).getSum()).isWithin(tolerance).of(arg.getSum());
+            return null;
+          }
+        },
+        new Function<SumDataLong, Void>() {
+          @Override
+          public Void apply(SumDataLong arg) {
+            assertThat(actual).isInstanceOf(SumDataLong.class);
+            assertThat(((SumDataLong) actual).getSum()).isEqualTo(arg.getSum());
+            return null;
+          }
+        },
+        new Function<CountData, Void>() {
+          @Override
+          public Void apply(CountData arg) {
+            assertThat(actual).isInstanceOf(CountData.class);
+            assertThat(((CountData) actual).getCount()).isEqualTo(arg.getCount());
+            return null;
+          }
+        },
+        new Function<DistributionData, Void>() {
+          @Override
+          public Void apply(DistributionData arg) {
+            assertThat(actual).isInstanceOf(DistributionData.class);
+            assertDistributionDataEquals(arg, (DistributionData) actual, tolerance);
+            return null;
+          }
+        },
+        new Function<LastValueDataDouble, Void>() {
+          @Override
+          public Void apply(LastValueDataDouble arg) {
+            assertThat(actual).isInstanceOf(LastValueDataDouble.class);
+            assertThat(((LastValueDataDouble) actual).getLastValue())
+                .isWithin(tolerance)
+                .of(arg.getLastValue());
+            return null;
+          }
+        },
+        new Function<LastValueDataLong, Void>() {
+          @Override
+          public Void apply(LastValueDataLong arg) {
+            assertThat(actual).isInstanceOf(LastValueDataLong.class);
+            assertThat(((LastValueDataLong) actual).getLastValue()).isEqualTo(arg.getLastValue());
+            return null;
+          }
+        },
+        new Function<AggregationData, Void>() {
+          @Override
+          public Void apply(AggregationData arg) {
+            if (arg instanceof MeanData) {
+              assertThat(actual).isInstanceOf(MeanData.class);
+              assertThat(((MeanData) actual).getMean())
+                  .isWithin(tolerance)
+                  .of(((MeanData) arg).getMean());
+              return null;
+            }
+            throw new IllegalArgumentException("Unknown Aggregation.");
+          }
+        });
+  }
+
+  // Create an empty ViewData with the given View.
+  static ViewData createEmptyViewData(View view) {
+    return ViewData.create(
+        view,
+        Collections.<List<TagValue>, AggregationData>emptyMap(),
+        view.getWindow()
+            .match(
+                Functions.<AggregationWindowData>returnConstant(
+                    CumulativeData.create(ZERO_TIMESTAMP, ZERO_TIMESTAMP)),
+                Functions.<AggregationWindowData>returnConstant(
+                    IntervalData.create(ZERO_TIMESTAMP)),
+                Functions.<AggregationWindowData>throwAssertionError()));
+  }
+
+  // Compare the expected and actual DistributionData within the given tolerance.
+  private static void assertDistributionDataEquals(
+      DistributionData expected, DistributionData actual, double tolerance) {
+    assertThat(actual.getMean()).isWithin(tolerance).of(expected.getMean());
+    assertThat(actual.getCount()).isEqualTo(expected.getCount());
+    assertThat(actual.getMean()).isWithin(tolerance).of(expected.getMean());
+    assertThat(actual.getSumOfSquaredDeviations())
+        .isWithin(tolerance)
+        .of(expected.getSumOfSquaredDeviations());
+
+    if (expected.getMax() == Double.NEGATIVE_INFINITY
+        && expected.getMin() == Double.POSITIVE_INFINITY) {
+      assertThat(actual.getMax()).isNegativeInfinity();
+      assertThat(actual.getMin()).isPositiveInfinity();
+    } else {
+      assertThat(actual.getMax()).isWithin(tolerance).of(expected.getMax());
+      assertThat(actual.getMin()).isWithin(tolerance).of(expected.getMin());
+    }
+
+    assertThat(removeTrailingZeros((actual).getBucketCounts()))
+        .isEqualTo(removeTrailingZeros(expected.getBucketCounts()));
+  }
+
+  @Nullable
+  private static List<Long> removeTrailingZeros(List<Long> longs) {
+    if (longs == null) {
+      return null;
+    }
+    List<Long> truncated = new ArrayList<Long>(longs);
+    while (!truncated.isEmpty() && Iterables.getLast(truncated) == 0) {
+      truncated.remove(truncated.size() - 1);
+    }
+    return truncated;
+  }
+
+  static final class SimpleTagContext extends TagContext {
+    private final List<Tag> tags;
+
+    SimpleTagContext(Tag... tags) {
+      this.tags = Collections.unmodifiableList(Lists.newArrayList(tags));
+    }
+
+    @Override
+    protected Iterator<Tag> getIterator() {
+      return tags.iterator();
+    }
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java
new file mode 100644
index 0000000..a4018b7
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java
@@ -0,0 +1,1021 @@
+/*
+ * 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.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.stats.StatsTestUtil.assertAggregationMapEquals;
+import static io.opencensus.implcore.stats.StatsTestUtil.createAggregationData;
+import static io.opencensus.implcore.stats.StatsTestUtil.createEmptyViewData;
+
+import com.google.common.collect.ImmutableMap;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.testing.common.TestClock;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ViewManagerImpl}. */
+@RunWith(JUnit4.class)
+public class ViewManagerImplTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private static final TagKey KEY = TagKey.create("KEY");
+
+  private static final TagValue VALUE = TagValue.create("VALUE");
+  private static final TagValue VALUE_2 = TagValue.create("VALUE_2");
+
+  private static final String MEASURE_NAME = "my measurement";
+
+  private static final String MEASURE_NAME_2 = "my measurement 2";
+
+  private static final String MEASURE_UNIT = "us";
+
+  private static final String MEASURE_DESCRIPTION = "measure description";
+
+  private static final MeasureDouble MEASURE_DOUBLE =
+      MeasureDouble.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT);
+
+  private static final MeasureLong MEASURE_LONG =
+      MeasureLong.create(MEASURE_NAME_2, MEASURE_DESCRIPTION, MEASURE_UNIT);
+
+  private static final Name VIEW_NAME = Name.create("my view");
+  private static final Name VIEW_NAME_2 = Name.create("my view 2");
+
+  private static final String VIEW_DESCRIPTION = "view description";
+
+  private static final Cumulative CUMULATIVE = Cumulative.create();
+
+  private static final double EPSILON = 1e-7;
+  private static final long MILLIS_PER_SECOND = 1000;
+  private static final Duration TEN_SECONDS = Duration.create(10, 0);
+  private static final Interval INTERVAL = Interval.create(TEN_SECONDS);
+
+  private static final BucketBoundaries BUCKET_BOUNDARIES =
+      BucketBoundaries.create(
+          Arrays.asList(
+              0.0, 0.2, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 7.0, 10.0, 15.0, 20.0, 30.0, 40.0, 50.0));
+
+  private static final Sum SUM = Sum.create();
+  private static final Mean MEAN = Mean.create();
+  private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+  private static final LastValue LAST_VALUE = LastValue.create();
+
+  private final TestClock clock = TestClock.create();
+
+  private final StatsComponentImplBase statsComponent =
+      new StatsComponentImplBase(new SimpleEventQueue(), clock);
+  private final TagsComponent tagsComponent = new TagsComponentImplBase();
+
+  private final Tagger tagger = tagsComponent.getTagger();
+  private final ViewManagerImpl viewManager = statsComponent.getViewManager();
+  private final StatsRecorderImpl statsRecorder = statsComponent.getStatsRecorder();
+
+  private static View createCumulativeView() {
+    return createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+  }
+
+  private static View createCumulativeView(
+      View.Name name, Measure measure, Aggregation aggregation, List<TagKey> keys) {
+    return View.create(name, VIEW_DESCRIPTION, measure, aggregation, keys, CUMULATIVE);
+  }
+
+  @Test
+  public void testRegisterAndGetCumulativeView() {
+    View view = createCumulativeView();
+    viewManager.registerView(view);
+    assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(view);
+    assertThat(viewManager.getView(VIEW_NAME).getAggregationMap()).isEmpty();
+    assertThat(viewManager.getView(VIEW_NAME).getWindowData()).isInstanceOf(CumulativeData.class);
+  }
+
+  @Test
+  public void testGetAllExportedViews() {
+    assertThat(viewManager.getAllExportedViews()).isEmpty();
+    View cumulativeView1 =
+        createCumulativeView(
+            View.Name.create("View 1"), MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+    View cumulativeView2 =
+        createCumulativeView(
+            View.Name.create("View 2"), MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+    View intervalView =
+        View.create(
+            View.Name.create("View 3"),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            INTERVAL);
+    viewManager.registerView(cumulativeView1);
+    viewManager.registerView(cumulativeView2);
+    viewManager.registerView(intervalView);
+
+    // Only cumulative views should be exported.
+    assertThat(viewManager.getAllExportedViews()).containsExactly(cumulativeView1, cumulativeView2);
+  }
+
+  @Test
+  public void getAllExportedViewsResultIsUnmodifiable() {
+    View view1 =
+        View.create(
+            View.Name.create("View 1"),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    viewManager.registerView(view1);
+    Set<View> exported = viewManager.getAllExportedViews();
+
+    View view2 =
+        View.create(
+            View.Name.create("View 2"),
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    thrown.expect(UnsupportedOperationException.class);
+    exported.add(view2);
+  }
+
+  @Test
+  public void testRegisterAndGetIntervalView() {
+    View intervalView =
+        View.create(
+            VIEW_NAME,
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            INTERVAL);
+    viewManager.registerView(intervalView);
+    assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(intervalView);
+    assertThat(viewManager.getView(VIEW_NAME).getAggregationMap()).isEmpty();
+    assertThat(viewManager.getView(VIEW_NAME).getWindowData()).isInstanceOf(IntervalData.class);
+  }
+
+  @Test
+  public void allowRegisteringSameViewTwice() {
+    View view = createCumulativeView();
+    viewManager.registerView(view);
+    viewManager.registerView(view);
+    assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(view);
+  }
+
+  @Test
+  public void preventRegisteringDifferentViewWithSameName() {
+    View view1 =
+        View.create(
+            VIEW_NAME,
+            "View description.",
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    View view2 =
+        View.create(
+            VIEW_NAME,
+            "This is a different description.",
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    testFailedToRegisterView(
+        view1, view2, "A different view with the same name is already registered");
+  }
+
+  @Test
+  public void preventRegisteringDifferentMeasureWithSameName() {
+    MeasureDouble measure1 = MeasureDouble.create("measure", "description", "1");
+    MeasureLong measure2 = MeasureLong.create("measure", "description", "1");
+    View view1 =
+        View.create(
+            VIEW_NAME, VIEW_DESCRIPTION, measure1, DISTRIBUTION, Arrays.asList(KEY), CUMULATIVE);
+    View view2 =
+        View.create(
+            VIEW_NAME_2, VIEW_DESCRIPTION, measure2, DISTRIBUTION, Arrays.asList(KEY), CUMULATIVE);
+    testFailedToRegisterView(
+        view1, view2, "A different measure with the same name is already registered");
+  }
+
+  private void testFailedToRegisterView(View view1, View view2, String message) {
+    viewManager.registerView(view1);
+    try {
+      thrown.expect(IllegalArgumentException.class);
+      thrown.expectMessage(message);
+      viewManager.registerView(view2);
+    } finally {
+      assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(view1);
+    }
+  }
+
+  @Test
+  public void returnNullWhenGettingNonexistentViewData() {
+    assertThat(viewManager.getView(VIEW_NAME)).isNull();
+  }
+
+  @Test
+  public void testRecordDouble_distribution_cumulative() {
+    testRecordCumulative(MEASURE_DOUBLE, DISTRIBUTION, 10.0, 20.0, 30.0, 40.0);
+  }
+
+  @Test
+  public void testRecordLong_distribution_cumulative() {
+    testRecordCumulative(MEASURE_LONG, DISTRIBUTION, 1000, 2000, 3000, 4000);
+  }
+
+  @Test
+  public void testRecordDouble_sum_cumulative() {
+    testRecordCumulative(MEASURE_DOUBLE, SUM, 11.1, 22.2, 33.3, 44.4);
+  }
+
+  @Test
+  public void testRecordLong_sum_cumulative() {
+    testRecordCumulative(MEASURE_LONG, SUM, 1000, 2000, 3000, 4000);
+  }
+
+  @Test
+  public void testRecordDouble_lastvalue_cumulative() {
+    testRecordCumulative(MEASURE_DOUBLE, LAST_VALUE, 11.1, 22.2, 33.3, 44.4);
+  }
+
+  @Test
+  public void testRecordLong_lastvalue_cumulative() {
+    testRecordCumulative(MEASURE_LONG, LAST_VALUE, 1000, 2000, 3000, 4000);
+  }
+
+  private void testRecordCumulative(Measure measure, Aggregation aggregation, double... values) {
+    View view = createCumulativeView(VIEW_NAME, measure, aggregation, Arrays.asList(KEY));
+    clock.setTime(Timestamp.create(1, 2));
+    viewManager.registerView(view);
+    TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+    for (double val : values) {
+      putToMeasureMap(statsRecorder.newMeasureMap(), measure, val).record(tags);
+    }
+    clock.setTime(Timestamp.create(3, 4));
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertThat(viewData.getView()).isEqualTo(view);
+    assertThat(viewData.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(1, 2), Timestamp.create(3, 4)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(aggregation, measure, values)),
+        EPSILON);
+  }
+
+  @Test
+  public void testRecordDouble_mean_interval() {
+    testRecordInterval(
+        MEASURE_DOUBLE,
+        MEAN,
+        new double[] {20.0, -1.0, 1.0, -5.0, 5.0},
+        9.0,
+        30.0,
+        MeanData.create((19 * 0.6 + 1) / 4, 4),
+        MeanData.create(0.2 * 5 + 9, 1),
+        MeanData.create(30.0, 1));
+  }
+
+  @Test
+  public void testRecordLong_mean_interval() {
+    testRecordInterval(
+        MEASURE_LONG,
+        MEAN,
+        new double[] {1000, 2000, 3000, 4000, 5000},
+        -5000,
+        30,
+        MeanData.create((3000 * 0.6 + 12000) / 4, 4),
+        MeanData.create(-4000, 1),
+        MeanData.create(30, 1));
+  }
+
+  @Test
+  public void testRecordDouble_sum_interval() {
+    testRecordInterval(
+        MEASURE_DOUBLE,
+        SUM,
+        new double[] {20.0, -1.0, 1.0, -5.0, 5.0},
+        9.0,
+        30.0,
+        SumDataDouble.create(19 * 0.6 + 1),
+        SumDataDouble.create(0.2 * 5 + 9),
+        SumDataDouble.create(30.0));
+  }
+
+  @Test
+  public void testRecordLong_sum_interval() {
+    testRecordInterval(
+        MEASURE_LONG,
+        SUM,
+        new double[] {10, 24, 30, 40, 50},
+        -50,
+        30,
+        SumDataLong.create(Math.round(34 * 0.6 + 120)),
+        SumDataLong.create(-40),
+        SumDataLong.create(30));
+  }
+
+  @Test
+  public void testRecordDouble_lastvalue_interval() {
+    testRecordInterval(
+        MEASURE_DOUBLE,
+        LAST_VALUE,
+        new double[] {20.0, -1.0, 1.0, -5.0, 5.0},
+        9.0,
+        30.0,
+        LastValueDataDouble.create(5.0),
+        LastValueDataDouble.create(9.0),
+        LastValueDataDouble.create(30.0));
+  }
+
+  @Test
+  public void testRecordLong_lastvalue_interval() {
+    testRecordInterval(
+        MEASURE_LONG,
+        LAST_VALUE,
+        new double[] {1000, 2000, 3000, 4000, 5000},
+        -5000,
+        30,
+        LastValueDataLong.create(5000),
+        LastValueDataLong.create(-5000),
+        LastValueDataLong.create(30));
+  }
+
+  private final void testRecordInterval(
+      Measure measure,
+      Aggregation aggregation,
+      double[] initialValues, /* There are 5 initial values recorded before we call getView(). */
+      double value6,
+      double value7,
+      AggregationData expectedValues1,
+      AggregationData expectedValues2,
+      AggregationData expectedValues3) {
+    // The interval is 10 seconds, i.e. values should expire after 10 seconds.
+    // Each bucket has a duration of 2.5 seconds.
+    View view =
+        View.create(
+            VIEW_NAME,
+            VIEW_DESCRIPTION,
+            measure,
+            aggregation,
+            Arrays.asList(KEY),
+            Interval.create(TEN_SECONDS));
+    long startTimeMillis = 30 * MILLIS_PER_SECOND; // start at 30s
+    clock.setTime(Timestamp.fromMillis(startTimeMillis));
+    viewManager.registerView(view);
+
+    TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+
+    for (int i = 1; i <= 5; i++) {
+      /*
+       * Add each value in sequence, at 31s, 32s, 33s, etc.
+       * 1st and 2nd values should fall into the first bucket [30.0, 32.5),
+       * 3rd and 4th values should fall into the second bucket [32.5, 35.0),
+       * 5th value should fall into the third bucket [35.0, 37.5).
+       */
+      clock.setTime(Timestamp.fromMillis(startTimeMillis + i * MILLIS_PER_SECOND));
+      putToMeasureMap(statsRecorder.newMeasureMap(), measure, initialValues[i - 1]).record(tags);
+    }
+
+    clock.setTime(Timestamp.fromMillis(startTimeMillis + 8 * MILLIS_PER_SECOND));
+    // 38s, no values should have expired
+    StatsTestUtil.assertAggregationMapEquals(
+        viewManager.getView(VIEW_NAME).getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(aggregation, measure, initialValues)),
+        EPSILON);
+
+    clock.setTime(Timestamp.fromMillis(startTimeMillis + 11 * MILLIS_PER_SECOND));
+    // 41s, 40% of the values in the first bucket should have expired (1 / 2.5 = 0.4).
+    StatsTestUtil.assertAggregationMapEquals(
+        viewManager.getView(VIEW_NAME).getAggregationMap(),
+        ImmutableMap.of(Arrays.asList(VALUE), expectedValues1),
+        EPSILON);
+
+    clock.setTime(Timestamp.fromMillis(startTimeMillis + 12 * MILLIS_PER_SECOND));
+    // 42s, add a new value value1, should fall into bucket [40.0, 42.5)
+    putToMeasureMap(statsRecorder.newMeasureMap(), measure, value6).record(tags);
+
+    clock.setTime(Timestamp.fromMillis(startTimeMillis + 17 * MILLIS_PER_SECOND));
+    // 47s, values in the first and second bucket should have expired, and 80% of values in the
+    // third bucket should have expired. The new value should persist.
+    StatsTestUtil.assertAggregationMapEquals(
+        viewManager.getView(VIEW_NAME).getAggregationMap(),
+        ImmutableMap.of(Arrays.asList(VALUE), expectedValues2),
+        EPSILON);
+
+    clock.setTime(Timestamp.fromMillis(60 * MILLIS_PER_SECOND));
+    // 60s, all previous values should have expired, add another value value2
+    putToMeasureMap(statsRecorder.newMeasureMap(), measure, value7).record(tags);
+    StatsTestUtil.assertAggregationMapEquals(
+        viewManager.getView(VIEW_NAME).getAggregationMap(),
+        ImmutableMap.of(Arrays.asList(VALUE), expectedValues3),
+        EPSILON);
+
+    clock.setTime(Timestamp.fromMillis(100 * MILLIS_PER_SECOND));
+    // 100s, all values should have expired
+    assertThat(viewManager.getView(VIEW_NAME).getAggregationMap()).isEmpty();
+  }
+
+  @Test
+  public void getViewDoesNotClearStats() {
+    View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+    clock.setTime(Timestamp.create(10, 0));
+    viewManager.registerView(view);
+    TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+    statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 0.1).record(tags);
+    clock.setTime(Timestamp.create(11, 0));
+    ViewData viewData1 = viewManager.getView(VIEW_NAME);
+    assertThat(viewData1.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(10, 0), Timestamp.create(11, 0)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData1.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 0.1)),
+        EPSILON);
+
+    statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 0.2).record(tags);
+    clock.setTime(Timestamp.create(12, 0));
+    ViewData viewData2 = viewManager.getView(VIEW_NAME);
+
+    // The second view should have the same start time as the first view, and it should include both
+    // recorded values:
+    assertThat(viewData2.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(10, 0), Timestamp.create(12, 0)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData2.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 0.1, 0.2)),
+        EPSILON);
+  }
+
+  @Test
+  public void testRecordCumulativeMultipleTagValues() {
+    viewManager.registerView(
+        createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY)));
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 10.0)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 30.0)
+        .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 50.0)
+        .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertAggregationMapEquals(
+        viewData.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 10.0),
+            Arrays.asList(VALUE_2),
+            createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 30.0, 50.0)),
+        EPSILON);
+  }
+
+  @Test
+  public void testRecordIntervalMultipleTagValues() {
+    // The interval is 10 seconds, i.e. values should expire after 10 seconds.
+    View view =
+        View.create(
+            VIEW_NAME,
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            Interval.create(TEN_SECONDS));
+    clock.setTime(Timestamp.create(10, 0)); // Start at 10s
+    viewManager.registerView(view);
+
+    // record for TagValue1 at 11s
+    clock.setTime(Timestamp.fromMillis(11 * MILLIS_PER_SECOND));
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 10.0)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+
+    // record for TagValue2 at 15s
+    clock.setTime(Timestamp.fromMillis(15 * MILLIS_PER_SECOND));
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 30.0)
+        .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 50.0)
+        .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+
+    // get ViewData at 19s, no stats should have expired.
+    clock.setTime(Timestamp.fromMillis(19 * MILLIS_PER_SECOND));
+    ViewData viewData1 = viewManager.getView(VIEW_NAME);
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData1.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 10.0),
+            Arrays.asList(VALUE_2),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 30.0, 50.0)),
+        EPSILON);
+
+    // record for TagValue2 again at 20s
+    clock.setTime(Timestamp.fromMillis(20 * MILLIS_PER_SECOND));
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 40.0)
+        .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+
+    // get ViewData at 25s, stats for TagValue1 should have expired.
+    clock.setTime(Timestamp.fromMillis(25 * MILLIS_PER_SECOND));
+    ViewData viewData2 = viewManager.getView(VIEW_NAME);
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData2.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE_2),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 30.0, 50.0, 40.0)),
+        EPSILON);
+
+    // get ViewData at 30s, the first two values for TagValue2 should have expired.
+    clock.setTime(Timestamp.fromMillis(30 * MILLIS_PER_SECOND));
+    ViewData viewData3 = viewManager.getView(VIEW_NAME);
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData3.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE_2),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 40.0)),
+        EPSILON);
+
+    // get ViewData at 40s, all stats should have expired.
+    clock.setTime(Timestamp.fromMillis(40 * MILLIS_PER_SECOND));
+    ViewData viewData4 = viewManager.getView(VIEW_NAME);
+    assertThat(viewData4.getAggregationMap()).isEmpty();
+  }
+
+  // This test checks that MeasureMaper.record(...) does not throw an exception when no views are
+  // registered.
+  @Test
+  public void allowRecordingWithoutRegisteringMatchingViewData() {
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 10)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+  }
+
+  @Test
+  public void testRecordWithEmptyStatsContext() {
+    viewManager.registerView(
+        createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY)));
+    // DEFAULT doesn't have tags, but the view has tag key "KEY".
+    statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 10.0).record(tagger.empty());
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertAggregationMapEquals(
+        viewData.getAggregationMap(),
+        ImmutableMap.of(
+            // Tag is missing for associated measureValues, should use default tag value
+            // "unknown/not set".
+            Arrays.asList(RecordUtils.UNKNOWN_TAG_VALUE),
+            // Should record stats with default tag value: "KEY" : "unknown/not set".
+            createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 10.0)),
+        EPSILON);
+  }
+
+  @Test
+  public void testRecord_MeasureNameNotMatch() {
+    testRecord_MeasureNotMatch(
+        MeasureDouble.create(MEASURE_NAME, "measure", MEASURE_UNIT),
+        MeasureDouble.create(MEASURE_NAME_2, "measure", MEASURE_UNIT),
+        10.0);
+  }
+
+  @Test
+  public void testRecord_MeasureTypeNotMatch() {
+    testRecord_MeasureNotMatch(
+        MeasureLong.create(MEASURE_NAME, "measure", MEASURE_UNIT),
+        MeasureDouble.create(MEASURE_NAME, "measure", MEASURE_UNIT),
+        10.0);
+  }
+
+  private void testRecord_MeasureNotMatch(Measure measure1, Measure measure2, double value) {
+    viewManager.registerView(createCumulativeView(VIEW_NAME, measure1, MEAN, Arrays.asList(KEY)));
+    TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+    putToMeasureMap(statsRecorder.newMeasureMap(), measure2, value).record(tags);
+    ViewData view = viewManager.getView(VIEW_NAME);
+    assertThat(view.getAggregationMap()).isEmpty();
+  }
+
+  @Test
+  public void testRecordWithTagsThatDoNotMatchViewData() {
+    viewManager.registerView(
+        createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY)));
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 10.0)
+        .record(tagger.emptyBuilder().put(TagKey.create("wrong key"), VALUE).build());
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 50.0)
+        .record(tagger.emptyBuilder().put(TagKey.create("another wrong key"), VALUE).build());
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertAggregationMapEquals(
+        viewData.getAggregationMap(),
+        ImmutableMap.of(
+            // Won't record the unregistered tag key, for missing registered keys will use default
+            // tag value : "unknown/not set".
+            Arrays.asList(RecordUtils.UNKNOWN_TAG_VALUE),
+            // Should record stats with default tag value: "KEY" : "unknown/not set".
+            createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 10.0, 50.0)),
+        EPSILON);
+  }
+
+  @Test
+  public void testViewDataWithMultipleTagKeys() {
+    TagKey key1 = TagKey.create("Key-1");
+    TagKey key2 = TagKey.create("Key-2");
+    viewManager.registerView(
+        createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(key1, key2)));
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.1)
+        .record(
+            tagger
+                .emptyBuilder()
+                .put(key1, TagValue.create("v1"))
+                .put(key2, TagValue.create("v10"))
+                .build());
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 2.2)
+        .record(
+            tagger
+                .emptyBuilder()
+                .put(key1, TagValue.create("v1"))
+                .put(key2, TagValue.create("v20"))
+                .build());
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 3.3)
+        .record(
+            tagger
+                .emptyBuilder()
+                .put(key1, TagValue.create("v2"))
+                .put(key2, TagValue.create("v10"))
+                .build());
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 4.4)
+        .record(
+            tagger
+                .emptyBuilder()
+                .put(key1, TagValue.create("v1"))
+                .put(key2, TagValue.create("v10"))
+                .build());
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertAggregationMapEquals(
+        viewData.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(TagValue.create("v1"), TagValue.create("v10")),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 1.1, 4.4),
+            Arrays.asList(TagValue.create("v1"), TagValue.create("v20")),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 2.2),
+            Arrays.asList(TagValue.create("v2"), TagValue.create("v10")),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 3.3)),
+        EPSILON);
+  }
+
+  @Test
+  public void testMultipleViewSameMeasure() {
+    final View view1 =
+        createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+    final View view2 =
+        createCumulativeView(VIEW_NAME_2, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+    clock.setTime(Timestamp.create(1, 1));
+    viewManager.registerView(view1);
+    clock.setTime(Timestamp.create(2, 2));
+    viewManager.registerView(view2);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 5.0)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+    clock.setTime(Timestamp.create(3, 3));
+    ViewData viewData1 = viewManager.getView(VIEW_NAME);
+    clock.setTime(Timestamp.create(4, 4));
+    ViewData viewData2 = viewManager.getView(VIEW_NAME_2);
+    assertThat(viewData1.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(1, 1), Timestamp.create(3, 3)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData1.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 5.0)),
+        EPSILON);
+    assertThat(viewData2.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(2, 2), Timestamp.create(4, 4)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData2.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 5.0)),
+        EPSILON);
+  }
+
+  @Test
+  public void testMultipleViews_DifferentMeasureNames() {
+    testMultipleViews_DifferentMeasures(
+        MeasureDouble.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT),
+        MeasureDouble.create(MEASURE_NAME_2, MEASURE_DESCRIPTION, MEASURE_UNIT),
+        1.1,
+        2.2);
+  }
+
+  @Test
+  public void testMultipleViews_DifferentMeasureTypes() {
+    testMultipleViews_DifferentMeasures(
+        MeasureDouble.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT),
+        MeasureLong.create(MEASURE_NAME_2, MEASURE_DESCRIPTION, MEASURE_UNIT),
+        1.1,
+        5000);
+  }
+
+  private void testMultipleViews_DifferentMeasures(
+      Measure measure1, Measure measure2, double value1, double value2) {
+    final View view1 = createCumulativeView(VIEW_NAME, measure1, DISTRIBUTION, Arrays.asList(KEY));
+    final View view2 =
+        createCumulativeView(VIEW_NAME_2, measure2, DISTRIBUTION, Arrays.asList(KEY));
+    clock.setTime(Timestamp.create(1, 0));
+    viewManager.registerView(view1);
+    clock.setTime(Timestamp.create(2, 0));
+    viewManager.registerView(view2);
+    TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+    MeasureMap measureMap = statsRecorder.newMeasureMap();
+    putToMeasureMap(measureMap, measure1, value1);
+    putToMeasureMap(measureMap, measure2, value2);
+    measureMap.record(tags);
+    clock.setTime(Timestamp.create(3, 0));
+    ViewData viewData1 = viewManager.getView(VIEW_NAME);
+    clock.setTime(Timestamp.create(4, 0));
+    ViewData viewData2 = viewManager.getView(VIEW_NAME_2);
+    assertThat(viewData1.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(1, 0), Timestamp.create(3, 0)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData1.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, measure1, value1)),
+        EPSILON);
+    assertThat(viewData2.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(2, 0), Timestamp.create(4, 0)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData2.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(DISTRIBUTION, measure2, value2)),
+        EPSILON);
+  }
+
+  @Test
+  public void testGetCumulativeViewDataWithEmptyBucketBoundaries() {
+    Aggregation noHistogram =
+        Distribution.create(BucketBoundaries.create(Collections.<Double>emptyList()));
+    View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, noHistogram, Arrays.asList(KEY));
+    clock.setTime(Timestamp.create(1, 0));
+    viewManager.registerView(view);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.1)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+    clock.setTime(Timestamp.create(3, 0));
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertThat(viewData.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(1, 0), Timestamp.create(3, 0)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(noHistogram, MEASURE_DOUBLE, 1.1)),
+        EPSILON);
+  }
+
+  @Test
+  public void testGetCumulativeViewDataWithoutBucketBoundaries() {
+    View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+    clock.setTime(Timestamp.create(1, 0));
+    viewManager.registerView(view);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.1)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+    clock.setTime(Timestamp.create(3, 0));
+    ViewData viewData = viewManager.getView(VIEW_NAME);
+    assertThat(viewData.getWindowData())
+        .isEqualTo(CumulativeData.create(Timestamp.create(1, 0), Timestamp.create(3, 0)));
+    StatsTestUtil.assertAggregationMapEquals(
+        viewData.getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE), StatsTestUtil.createAggregationData(MEAN, MEASURE_DOUBLE, 1.1)),
+        EPSILON);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void registerRecordAndGetView_StatsDisabled() {
+    statsComponent.setState(StatsCollectionState.DISABLED);
+    View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+    viewManager.registerView(view);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.1)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+    assertThat(viewManager.getView(VIEW_NAME)).isEqualTo(createEmptyViewData(view));
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void registerRecordAndGetView_StatsReenabled() {
+    statsComponent.setState(StatsCollectionState.DISABLED);
+    statsComponent.setState(StatsCollectionState.ENABLED);
+    View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+    viewManager.registerView(view);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.1)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+    StatsTestUtil.assertAggregationMapEquals(
+        viewManager.getView(VIEW_NAME).getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE), StatsTestUtil.createAggregationData(MEAN, MEASURE_DOUBLE, 1.1)),
+        EPSILON);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void registerViewWithStatsDisabled_RecordAndGetViewWithStatsEnabled() {
+    statsComponent.setState(StatsCollectionState.DISABLED);
+    View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+    viewManager.registerView(view); // view will still be registered.
+
+    statsComponent.setState(StatsCollectionState.ENABLED);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.1)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+    StatsTestUtil.assertAggregationMapEquals(
+        viewManager.getView(VIEW_NAME).getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE), StatsTestUtil.createAggregationData(MEAN, MEASURE_DOUBLE, 1.1)),
+        EPSILON);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void registerDifferentViewWithSameNameWithStatsDisabled() {
+    statsComponent.setState(StatsCollectionState.DISABLED);
+    View view1 =
+        View.create(
+            VIEW_NAME,
+            "View description.",
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    View view2 =
+        View.create(
+            VIEW_NAME,
+            "This is a different description.",
+            MEASURE_DOUBLE,
+            DISTRIBUTION,
+            Arrays.asList(KEY),
+            CUMULATIVE);
+    testFailedToRegisterView(
+        view1, view2, "A different view with the same name is already registered");
+  }
+
+  @Test
+  public void settingStateToDisabledWillClearStats_Cumulative() {
+    View cumulativeView = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+    settingStateToDisabledWillClearStats(cumulativeView);
+  }
+
+  @Test
+  public void settingStateToDisabledWillClearStats_Interval() {
+    View intervalView =
+        View.create(
+            VIEW_NAME_2,
+            VIEW_DESCRIPTION,
+            MEASURE_DOUBLE,
+            MEAN,
+            Arrays.asList(KEY),
+            Interval.create(Duration.create(60, 0)));
+    settingStateToDisabledWillClearStats(intervalView);
+  }
+
+  @SuppressWarnings("deprecation")
+  private void settingStateToDisabledWillClearStats(View view) {
+    Timestamp timestamp1 = Timestamp.create(1, 0);
+    clock.setTime(timestamp1);
+    viewManager.registerView(view);
+    statsRecorder
+        .newMeasureMap()
+        .put(MEASURE_DOUBLE, 1.1)
+        .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+    StatsTestUtil.assertAggregationMapEquals(
+        viewManager.getView(view.getName()).getAggregationMap(),
+        ImmutableMap.of(
+            Arrays.asList(VALUE),
+            StatsTestUtil.createAggregationData(view.getAggregation(), view.getMeasure(), 1.1)),
+        EPSILON);
+
+    Timestamp timestamp2 = Timestamp.create(2, 0);
+    clock.setTime(timestamp2);
+    statsComponent.setState(StatsCollectionState.DISABLED); // This will clear stats.
+    assertThat(viewManager.getView(view.getName())).isEqualTo(createEmptyViewData(view));
+
+    Timestamp timestamp3 = Timestamp.create(3, 0);
+    clock.setTime(timestamp3);
+    statsComponent.setState(StatsCollectionState.ENABLED);
+
+    Timestamp timestamp4 = Timestamp.create(4, 0);
+    clock.setTime(timestamp4);
+    // This ViewData does not have any stats, but it should not be an empty ViewData, since it has
+    // non-zero TimeStamps.
+    ViewData viewData = viewManager.getView(view.getName());
+    assertThat(viewData.getAggregationMap()).isEmpty();
+    AggregationWindowData windowData = viewData.getWindowData();
+    if (windowData instanceof CumulativeData) {
+      assertThat(windowData).isEqualTo(CumulativeData.create(timestamp3, timestamp4));
+    } else {
+      assertThat(windowData).isEqualTo(IntervalData.create(timestamp4));
+    }
+  }
+
+  private static MeasureMap putToMeasureMap(MeasureMap measureMap, Measure measure, double value) {
+    if (measure instanceof MeasureDouble) {
+      return measureMap.put((MeasureDouble) measure, value);
+    } else if (measure instanceof MeasureLong) {
+      return measureMap.put((MeasureLong) measure, Math.round(value));
+    } else {
+      // Future measures.
+      throw new AssertionError();
+    }
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTagContextUtilsTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTagContextUtilsTest.java
new file mode 100644
index 0000000..1a14ac6
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTagContextUtilsTest.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.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.tags.TagsTestUtil.tagContextToList;
+
+import com.google.common.collect.ImmutableSet;
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.unsafe.ContextUtils;
+import java.util.Iterator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link CurrentTagContextUtils}. */
+@RunWith(JUnit4.class)
+public class CurrentTagContextUtilsTest {
+  private static final Tag TAG = Tag.create(TagKey.create("key"), TagValue.create("value"));
+
+  private final TagContext tagContext =
+      new TagContext() {
+
+        @Override
+        protected Iterator<Tag> getIterator() {
+          return ImmutableSet.<Tag>of(TAG).iterator();
+        }
+      };
+
+  @Test
+  public void testGetCurrentTagContext_DefaultContext() {
+    TagContext tags = CurrentTagContextUtils.getCurrentTagContext();
+    assertThat(tags).isNotNull();
+    assertThat(tagContextToList(tags)).isEmpty();
+  }
+
+  @Test
+  public void testGetCurrentTagContext_ContextSetToNull() {
+    Context orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, null).attach();
+    try {
+      TagContext tags = CurrentTagContextUtils.getCurrentTagContext();
+      assertThat(tags).isNotNull();
+      assertThat(tagContextToList(tags)).isEmpty();
+    } finally {
+      Context.current().detach(orig);
+    }
+  }
+
+  @Test
+  public void testWithTagContext() {
+    assertThat(tagContextToList(CurrentTagContextUtils.getCurrentTagContext())).isEmpty();
+    Scope scopedTags = CurrentTagContextUtils.withTagContext(tagContext);
+    try {
+      assertThat(CurrentTagContextUtils.getCurrentTagContext()).isSameAs(tagContext);
+    } finally {
+      scopedTags.close();
+    }
+    assertThat(tagContextToList(CurrentTagContextUtils.getCurrentTagContext())).isEmpty();
+  }
+
+  @Test
+  public void testWithTagContextUsingWrap() {
+    Runnable runnable;
+    Scope scopedTags = CurrentTagContextUtils.withTagContext(tagContext);
+    try {
+      assertThat(CurrentTagContextUtils.getCurrentTagContext()).isSameAs(tagContext);
+      runnable =
+          Context.current()
+              .wrap(
+                  new Runnable() {
+                    @Override
+                    public void run() {
+                      assertThat(CurrentTagContextUtils.getCurrentTagContext())
+                          .isSameAs(tagContext);
+                    }
+                  });
+    } finally {
+      scopedTags.close();
+    }
+    assertThat(tagContextToList(CurrentTagContextUtils.getCurrentTagContext())).isEmpty();
+    // When we run the runnable we will have the TagContext in the current Context.
+    runnable.run();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/ScopedTagContextsTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/ScopedTagContextsTest.java
new file mode 100644
index 0000000..6a8fe4c
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/ScopedTagContextsTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.tags.TagsTestUtil.tagContextToList;
+
+import io.opencensus.common.Scope;
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for the methods in {@link TaggerImpl} and {@link TagContextBuilderImpl} that interact
+ * with the current {@link TagContext}.
+ */
+@RunWith(JUnit4.class)
+public class ScopedTagContextsTest {
+  private static final TagKey KEY_1 = TagKey.create("key 1");
+  private static final TagKey KEY_2 = TagKey.create("key 2");
+
+  private static final TagValue VALUE_1 = TagValue.create("value 1");
+  private static final TagValue VALUE_2 = TagValue.create("value 2");
+
+  private final Tagger tagger = new TaggerImpl(new CurrentState(State.ENABLED));
+
+  @Test
+  public void defaultTagContext() {
+    TagContext defaultTagContext = tagger.getCurrentTagContext();
+    assertThat(tagContextToList(defaultTagContext)).isEmpty();
+    assertThat(defaultTagContext).isInstanceOf(TagContextImpl.class);
+  }
+
+  @Test
+  public void withTagContext() {
+    assertThat(tagContextToList(tagger.getCurrentTagContext())).isEmpty();
+    TagContext scopedTags = tagger.emptyBuilder().put(KEY_1, VALUE_1).build();
+    Scope scope = tagger.withTagContext(scopedTags);
+    try {
+      assertThat(tagger.getCurrentTagContext()).isSameAs(scopedTags);
+    } finally {
+      scope.close();
+    }
+    assertThat(tagContextToList(tagger.getCurrentTagContext())).isEmpty();
+  }
+
+  @Test
+  public void createBuilderFromCurrentTags() {
+    TagContext scopedTags = tagger.emptyBuilder().put(KEY_1, VALUE_1).build();
+    Scope scope = tagger.withTagContext(scopedTags);
+    try {
+      TagContext newTags = tagger.currentBuilder().put(KEY_2, VALUE_2).build();
+      assertThat(tagContextToList(newTags))
+          .containsExactly(Tag.create(KEY_1, VALUE_1), Tag.create(KEY_2, VALUE_2));
+      assertThat(tagger.getCurrentTagContext()).isSameAs(scopedTags);
+    } finally {
+      scope.close();
+    }
+  }
+
+  @Test
+  public void setCurrentTagsWithBuilder() {
+    assertThat(tagContextToList(tagger.getCurrentTagContext())).isEmpty();
+    Scope scope = tagger.emptyBuilder().put(KEY_1, VALUE_1).buildScoped();
+    try {
+      assertThat(tagContextToList(tagger.getCurrentTagContext()))
+          .containsExactly(Tag.create(KEY_1, VALUE_1));
+    } finally {
+      scope.close();
+    }
+    assertThat(tagContextToList(tagger.getCurrentTagContext())).isEmpty();
+  }
+
+  @Test
+  public void addToCurrentTagsWithBuilder() {
+    TagContext scopedTags = tagger.emptyBuilder().put(KEY_1, VALUE_1).build();
+    Scope scope1 = tagger.withTagContext(scopedTags);
+    try {
+      Scope scope2 = tagger.currentBuilder().put(KEY_2, VALUE_2).buildScoped();
+      try {
+        assertThat(tagContextToList(tagger.getCurrentTagContext()))
+            .containsExactly(Tag.create(KEY_1, VALUE_1), Tag.create(KEY_2, VALUE_2));
+      } finally {
+        scope2.close();
+      }
+      assertThat(tagger.getCurrentTagContext()).isSameAs(scopedTags);
+    } finally {
+      scope1.close();
+    }
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/TagContextImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/TagContextImplTest.java
new file mode 100644
index 0000000..1859e08
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TagContextImplTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link TagContextImpl} and {@link TagContextBuilderImpl}.
+ *
+ * <p>Tests for {@link TagContextBuilderImpl#buildScoped()} are in {@link ScopedTagContextsTest}.
+ */
+@RunWith(JUnit4.class)
+public class TagContextImplTest {
+  private final Tagger tagger = new TaggerImpl(new CurrentState(State.ENABLED));
+
+  private static final TagKey K1 = TagKey.create("k1");
+  private static final TagKey K2 = TagKey.create("k2");
+
+  private static final TagValue V1 = TagValue.create("v1");
+  private static final TagValue V2 = TagValue.create("v2");
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void getTags_empty() {
+    TagContextImpl tags = new TagContextImpl(ImmutableMap.<TagKey, TagValue>of());
+    assertThat(tags.getTags()).isEmpty();
+  }
+
+  @Test
+  public void getTags_nonEmpty() {
+    TagContextImpl tags = new TagContextImpl(ImmutableMap.of(K1, V1, K2, V2));
+    assertThat(tags.getTags()).containsExactly(K1, V1, K2, V2);
+  }
+
+  @Test
+  public void put_newKey() {
+    TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+    assertThat(((TagContextImpl) tagger.toBuilder(tags).put(K2, V2).build()).getTags())
+        .containsExactly(K1, V1, K2, V2);
+  }
+
+  @Test
+  public void put_existingKey() {
+    TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+    assertThat(((TagContextImpl) tagger.toBuilder(tags).put(K1, V2).build()).getTags())
+        .containsExactly(K1, V2);
+  }
+
+  @Test
+  public void put_nullKey() {
+    TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+    TagContextBuilder builder = tagger.toBuilder(tags);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("key");
+    builder.put(null, V2);
+  }
+
+  @Test
+  public void put_nullValue() {
+    TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+    TagContextBuilder builder = tagger.toBuilder(tags);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("value");
+    builder.put(K2, null);
+  }
+
+  @Test
+  public void remove_existingKey() {
+    TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1, K2, V2));
+    assertThat(((TagContextImpl) tagger.toBuilder(tags).remove(K1).build()).getTags())
+        .containsExactly(K2, V2);
+  }
+
+  @Test
+  public void remove_differentKey() {
+    TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+    assertThat(((TagContextImpl) tagger.toBuilder(tags).remove(K2).build()).getTags())
+        .containsExactly(K1, V1);
+  }
+
+  @Test
+  public void remove_nullKey() {
+    TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+    TagContextBuilder builder = tagger.toBuilder(tags);
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("key");
+    builder.remove(null);
+  }
+
+  @Test
+  public void testIterator() {
+    TagContextImpl tags = new TagContextImpl(ImmutableMap.of(K1, V1, K2, V2));
+    Iterator<Tag> i = tags.getIterator();
+    assertTrue(i.hasNext());
+    Tag tag1 = i.next();
+    assertTrue(i.hasNext());
+    Tag tag2 = i.next();
+    assertFalse(i.hasNext());
+    assertThat(Arrays.asList(tag1, tag2)).containsExactly(Tag.create(K1, V1), Tag.create(K2, V2));
+    thrown.expect(NoSuchElementException.class);
+    i.next();
+  }
+
+  @Test
+  public void disallowCallingRemoveOnIterator() {
+    TagContextImpl tags = new TagContextImpl(ImmutableMap.of(K1, V1, K2, V2));
+    Iterator<Tag> i = tags.getIterator();
+    i.next();
+    thrown.expect(UnsupportedOperationException.class);
+    i.remove();
+  }
+
+  @Test
+  public void testEquals() {
+    new EqualsTester()
+        .addEqualityGroup(
+            tagger.emptyBuilder().put(K1, V1).put(K2, V2).build(),
+            tagger.emptyBuilder().put(K1, V1).put(K2, V2).build(),
+            tagger.emptyBuilder().put(K2, V2).put(K1, V1).build(),
+            new TagContext() {
+              @Override
+              protected Iterator<Tag> getIterator() {
+                return Lists.<Tag>newArrayList(Tag.create(K1, V1), Tag.create(K2, V2)).iterator();
+              }
+            })
+        .addEqualityGroup(tagger.emptyBuilder().put(K1, V1).put(K2, V1).build())
+        .addEqualityGroup(tagger.emptyBuilder().put(K1, V2).put(K2, V1).build())
+        .testEquals();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/TaggerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/TaggerImplTest.java
new file mode 100644
index 0000000..4ca2ae7
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TaggerImplTest.java
@@ -0,0 +1,318 @@
+/*
+ * 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.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.tags.TagsTestUtil.tagContextToList;
+
+import com.google.common.collect.Lists;
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.implcore.internal.NoopScope;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.unsafe.ContextUtils;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TaggerImpl}. */
+@RunWith(JUnit4.class)
+public class TaggerImplTest {
+  private final TagsComponent tagsComponent = new TagsComponentImplBase();
+  private final Tagger tagger = tagsComponent.getTagger();
+
+  private static final TagKey K1 = TagKey.create("k1");
+  private static final TagKey K2 = TagKey.create("k2");
+  private static final TagKey K3 = TagKey.create("k3");
+
+  private static final TagValue V1 = TagValue.create("v1");
+  private static final TagValue V2 = TagValue.create("v2");
+  private static final TagValue V3 = TagValue.create("v3");
+
+  private static final Tag TAG1 = Tag.create(K1, V1);
+  private static final Tag TAG2 = Tag.create(K2, V2);
+  private static final Tag TAG3 = Tag.create(K3, V3);
+
+  @Test
+  public void empty() {
+    assertThat(tagContextToList(tagger.empty())).isEmpty();
+    assertThat(tagger.empty()).isInstanceOf(TagContextImpl.class);
+  }
+
+  @Test
+  public void empty_TaggingDisabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagContextToList(tagger.empty())).isEmpty();
+    assertThat(tagger.empty()).isInstanceOf(TagContextImpl.class);
+  }
+
+  @Test
+  public void emptyBuilder() {
+    TagContextBuilder builder = tagger.emptyBuilder();
+    assertThat(builder).isInstanceOf(TagContextBuilderImpl.class);
+    assertThat(tagContextToList(builder.build())).isEmpty();
+  }
+
+  @Test
+  public void emptyBuilder_TaggingDisabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagger.emptyBuilder()).isSameAs(NoopTagContextBuilder.INSTANCE);
+  }
+
+  @Test
+  public void emptyBuilder_TaggingReenabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagger.emptyBuilder()).isSameAs(NoopTagContextBuilder.INSTANCE);
+    tagsComponent.setState(TaggingState.ENABLED);
+    TagContextBuilder builder = tagger.emptyBuilder();
+    assertThat(builder).isInstanceOf(TagContextBuilderImpl.class);
+    assertThat(tagContextToList(builder.put(K1, V1).build())).containsExactly(Tag.create(K1, V1));
+  }
+
+  @Test
+  public void currentBuilder() {
+    TagContext tags = new SimpleTagContext(TAG1, TAG2, TAG3);
+    TagContextBuilder result = getResultOfCurrentBuilder(tags);
+    assertThat(result).isInstanceOf(TagContextBuilderImpl.class);
+    assertThat(tagContextToList(result.build())).containsExactly(TAG1, TAG2, TAG3);
+  }
+
+  @Test
+  public void currentBuilder_DefaultIsEmpty() {
+    TagContextBuilder currentBuilder = tagger.currentBuilder();
+    assertThat(currentBuilder).isInstanceOf(TagContextBuilderImpl.class);
+    assertThat(tagContextToList(currentBuilder.build())).isEmpty();
+  }
+
+  @Test
+  public void currentBuilder_RemoveDuplicateTags() {
+    Tag tag1 = Tag.create(K1, V1);
+    Tag tag2 = Tag.create(K1, V2);
+    TagContext tagContextWithDuplicateTags = new SimpleTagContext(tag1, tag2);
+    TagContextBuilder result = getResultOfCurrentBuilder(tagContextWithDuplicateTags);
+    assertThat(tagContextToList(result.build())).containsExactly(tag2);
+  }
+
+  @Test
+  public void currentBuilder_SkipNullTag() {
+    TagContext tagContextWithNullTag = new SimpleTagContext(TAG1, null, TAG2);
+    TagContextBuilder result = getResultOfCurrentBuilder(tagContextWithNullTag);
+    assertThat(tagContextToList(result.build())).containsExactly(TAG1, TAG2);
+  }
+
+  @Test
+  public void currentBuilder_TaggingDisabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(getResultOfCurrentBuilder(new SimpleTagContext(TAG1)))
+        .isSameAs(NoopTagContextBuilder.INSTANCE);
+  }
+
+  @Test
+  public void currentBuilder_TaggingReenabled() {
+    TagContext tags = new SimpleTagContext(TAG1);
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(getResultOfCurrentBuilder(tags)).isSameAs(NoopTagContextBuilder.INSTANCE);
+    tagsComponent.setState(TaggingState.ENABLED);
+    TagContextBuilder builder = getResultOfCurrentBuilder(tags);
+    assertThat(builder).isInstanceOf(TagContextBuilderImpl.class);
+    assertThat(tagContextToList(builder.build())).containsExactly(TAG1);
+  }
+
+  private TagContextBuilder getResultOfCurrentBuilder(TagContext tagsToSet) {
+    Context orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, tagsToSet).attach();
+    try {
+      return tagger.currentBuilder();
+    } finally {
+      Context.current().detach(orig);
+    }
+  }
+
+  @Test
+  public void toBuilder_ConvertUnknownTagContextToTagContextImpl() {
+    TagContext unknownTagContext = new SimpleTagContext(TAG1, TAG2, TAG3);
+    TagContext newTagContext = tagger.toBuilder(unknownTagContext).build();
+    assertThat(tagContextToList(newTagContext)).containsExactly(TAG1, TAG2, TAG3);
+    assertThat(newTagContext).isInstanceOf(TagContextImpl.class);
+  }
+
+  @Test
+  public void toBuilder_RemoveDuplicatesFromUnknownTagContext() {
+    Tag tag1 = Tag.create(K1, V1);
+    Tag tag2 = Tag.create(K1, V2);
+    TagContext tagContextWithDuplicateTags = new SimpleTagContext(tag1, tag2);
+    TagContext newTagContext = tagger.toBuilder(tagContextWithDuplicateTags).build();
+    assertThat(tagContextToList(newTagContext)).containsExactly(tag2);
+  }
+
+  @Test
+  public void toBuilder_SkipNullTag() {
+    TagContext tagContextWithNullTag = new SimpleTagContext(TAG1, null, TAG2);
+    TagContext newTagContext = tagger.toBuilder(tagContextWithNullTag).build();
+    assertThat(tagContextToList(newTagContext)).containsExactly(TAG1, TAG2);
+  }
+
+  @Test
+  public void toBuilder_TaggingDisabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagger.toBuilder(new SimpleTagContext(TAG1)))
+        .isSameAs(NoopTagContextBuilder.INSTANCE);
+  }
+
+  @Test
+  public void toBuilder_TaggingReenabled() {
+    TagContext tags = new SimpleTagContext(TAG1);
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagger.toBuilder(tags)).isSameAs(NoopTagContextBuilder.INSTANCE);
+    tagsComponent.setState(TaggingState.ENABLED);
+    TagContextBuilder builder = tagger.toBuilder(tags);
+    assertThat(builder).isInstanceOf(TagContextBuilderImpl.class);
+    assertThat(tagContextToList(builder.build())).containsExactly(TAG1);
+  }
+
+  @Test
+  public void getCurrentTagContext_DefaultIsEmptyTagContextImpl() {
+    TagContext currentTagContext = tagger.getCurrentTagContext();
+    assertThat(tagContextToList(currentTagContext)).isEmpty();
+    assertThat(currentTagContext).isInstanceOf(TagContextImpl.class);
+  }
+
+  @Test
+  public void getCurrentTagContext_ConvertUnknownTagContextToTagContextImpl() {
+    TagContext unknownTagContext = new SimpleTagContext(TAG1, TAG2, TAG3);
+    TagContext result = getResultOfGetCurrentTagContext(unknownTagContext);
+    assertThat(result).isInstanceOf(TagContextImpl.class);
+    assertThat(tagContextToList(result)).containsExactly(TAG1, TAG2, TAG3);
+  }
+
+  @Test
+  public void getCurrentTagContext_RemoveDuplicatesFromUnknownTagContext() {
+    Tag tag1 = Tag.create(K1, V1);
+    Tag tag2 = Tag.create(K1, V2);
+    TagContext tagContextWithDuplicateTags = new SimpleTagContext(tag1, tag2);
+    TagContext result = getResultOfGetCurrentTagContext(tagContextWithDuplicateTags);
+    assertThat(tagContextToList(result)).containsExactly(tag2);
+  }
+
+  @Test
+  public void getCurrentTagContext_SkipNullTag() {
+    TagContext tagContextWithNullTag = new SimpleTagContext(TAG1, null, TAG2);
+    TagContext result = getResultOfGetCurrentTagContext(tagContextWithNullTag);
+    assertThat(tagContextToList(result)).containsExactly(TAG1, TAG2);
+  }
+
+  @Test
+  public void getCurrentTagContext_TaggingDisabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagContextToList(getResultOfGetCurrentTagContext(new SimpleTagContext(TAG1))))
+        .isEmpty();
+  }
+
+  @Test
+  public void getCurrentTagContext_TaggingReenabled() {
+    TagContext tags = new SimpleTagContext(TAG1);
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagContextToList(getResultOfGetCurrentTagContext(tags))).isEmpty();
+    tagsComponent.setState(TaggingState.ENABLED);
+    assertThat(tagContextToList(getResultOfGetCurrentTagContext(tags))).containsExactly(TAG1);
+  }
+
+  private TagContext getResultOfGetCurrentTagContext(TagContext tagsToSet) {
+    Context orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, tagsToSet).attach();
+    try {
+      return tagger.getCurrentTagContext();
+    } finally {
+      Context.current().detach(orig);
+    }
+  }
+
+  @Test
+  public void withTagContext_ConvertUnknownTagContextToTagContextImpl() {
+    TagContext unknownTagContext = new SimpleTagContext(TAG1, TAG2, TAG3);
+    TagContext result = getResultOfWithTagContext(unknownTagContext);
+    assertThat(result).isInstanceOf(TagContextImpl.class);
+    assertThat(tagContextToList(result)).containsExactly(TAG1, TAG2, TAG3);
+  }
+
+  @Test
+  public void withTagContext_RemoveDuplicatesFromUnknownTagContext() {
+    Tag tag1 = Tag.create(K1, V1);
+    Tag tag2 = Tag.create(K1, V2);
+    TagContext tagContextWithDuplicateTags = new SimpleTagContext(tag1, tag2);
+    TagContext result = getResultOfWithTagContext(tagContextWithDuplicateTags);
+    assertThat(tagContextToList(result)).containsExactly(tag2);
+  }
+
+  @Test
+  public void withTagContext_SkipNullTag() {
+    TagContext tagContextWithNullTag = new SimpleTagContext(TAG1, null, TAG2);
+    TagContext result = getResultOfWithTagContext(tagContextWithNullTag);
+    assertThat(tagContextToList(result)).containsExactly(TAG1, TAG2);
+  }
+
+  @Test
+  public void withTagContext_ReturnsNoopScopeWhenTaggingIsDisabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagger.withTagContext(new SimpleTagContext(TAG1))).isSameAs(NoopScope.getInstance());
+  }
+
+  @Test
+  public void withTagContext_TaggingDisabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagContextToList(getResultOfWithTagContext(new SimpleTagContext(TAG1)))).isEmpty();
+  }
+
+  @Test
+  public void withTagContext_TaggingReenabled() {
+    TagContext tags = new SimpleTagContext(TAG1);
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagContextToList(getResultOfWithTagContext(tags))).isEmpty();
+    tagsComponent.setState(TaggingState.ENABLED);
+    assertThat(tagContextToList(getResultOfWithTagContext(tags))).containsExactly(TAG1);
+  }
+
+  private TagContext getResultOfWithTagContext(TagContext tagsToSet) {
+    Scope scope = tagger.withTagContext(tagsToSet);
+    try {
+      return ContextUtils.TAG_CONTEXT_KEY.get();
+    } finally {
+      scope.close();
+    }
+  }
+
+  private static final class SimpleTagContext extends TagContext {
+    private final List<Tag> tags;
+
+    SimpleTagContext(Tag... tags) {
+      this.tags = Collections.unmodifiableList(Lists.newArrayList(tags));
+    }
+
+    @Override
+    protected Iterator<Tag> getIterator() {
+      return tags.iterator();
+    }
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/TagsComponentImplBaseTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsComponentImplBaseTest.java
new file mode 100644
index 0000000..1bc13c5
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsComponentImplBaseTest.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.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TagsComponentImplBase}. */
+@RunWith(JUnit4.class)
+public class TagsComponentImplBaseTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private final TagsComponent tagsComponent = new TagsComponentImplBase();
+
+  @Test
+  public void defaultState() {
+    assertThat(tagsComponent.getState()).isEqualTo(TaggingState.ENABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void setState_Disabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(tagsComponent.getState()).isEqualTo(TaggingState.DISABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void setState_Enabled() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    tagsComponent.setState(TaggingState.ENABLED);
+    assertThat(tagsComponent.getState()).isEqualTo(TaggingState.ENABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void setState_DisallowsNull() {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("newState");
+    tagsComponent.setState(null);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void preventSettingStateAfterGettingState_DifferentState() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    tagsComponent.getState();
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("State was already read, cannot set state.");
+    tagsComponent.setState(TaggingState.ENABLED);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void preventSettingStateAfterGettingState_SameState() {
+    tagsComponent.setState(TaggingState.DISABLED);
+    tagsComponent.getState();
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("State was already read, cannot set state.");
+    tagsComponent.setState(TaggingState.DISABLED);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/TagsTestUtil.java b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsTestUtil.java
new file mode 100644
index 0000000..dcfba50
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsTestUtil.java
@@ -0,0 +1,33 @@
+/*
+ * 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.implcore.tags;
+
+import com.google.common.collect.Lists;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import java.util.Collection;
+
+/** Test utilities for tagging. */
+public class TagsTestUtil {
+  private TagsTestUtil() {}
+
+  /** Returns a collection of all tags in a {@link TagContext}. */
+  public static Collection<Tag> tagContextToList(TagContext tags) {
+    return Lists.newArrayList(InternalUtils.getTags(tags));
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImplTest.java
new file mode 100644
index 0000000..26a072f
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImplTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.implcore.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.implcore.tags.TagsTestUtil;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import io.opencensus.tags.propagation.TagContextSerializationException;
+import java.util.Iterator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link TagContextBinarySerializerImpl}.
+ *
+ * <p>Thorough serialization/deserialization tests are in {@link TagContextSerializationTest},
+ * {@link TagContextDeserializationTest}, and {@link TagContextRoundtripTest}.
+ */
+@RunWith(JUnit4.class)
+public final class TagContextBinarySerializerImplTest {
+  private final TagsComponent tagsComponent = new TagsComponentImplBase();
+  private final TagContextBinarySerializer serializer =
+      tagsComponent.getTagPropagationComponent().getBinarySerializer();
+
+  private final TagContext tagContext =
+      new TagContext() {
+        @Override
+        public Iterator<Tag> getIterator() {
+          return ImmutableSet.<Tag>of(Tag.create(TagKey.create("key"), TagValue.create("value")))
+              .iterator();
+        }
+      };
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void toByteArray_TaggingDisabled() throws TagContextSerializationException {
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(serializer.toByteArray(tagContext)).isEmpty();
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void toByteArray_TaggingReenabled() throws TagContextSerializationException {
+    final byte[] serialized = serializer.toByteArray(tagContext);
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(serializer.toByteArray(tagContext)).isEmpty();
+    tagsComponent.setState(TaggingState.ENABLED);
+    assertThat(serializer.toByteArray(tagContext)).isEqualTo(serialized);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void fromByteArray_TaggingDisabled()
+      throws TagContextDeserializationException, TagContextSerializationException {
+    byte[] serialized = serializer.toByteArray(tagContext);
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(TagsTestUtil.tagContextToList(serializer.fromByteArray(serialized))).isEmpty();
+  }
+
+  @Test
+  public void fromByteArray_TaggingReenabled()
+      throws TagContextDeserializationException, TagContextSerializationException {
+    byte[] serialized = serializer.toByteArray(tagContext);
+    tagsComponent.setState(TaggingState.DISABLED);
+    assertThat(TagsTestUtil.tagContextToList(serializer.fromByteArray(serialized))).isEmpty();
+    tagsComponent.setState(TaggingState.ENABLED);
+    assertThat(serializer.fromByteArray(serialized)).isEqualTo(tagContext);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextDeserializationTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextDeserializationTest.java
new file mode 100644
index 0000000..8db0e38
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextDeserializationTest.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2016-17, 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.implcore.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.ByteArrayDataOutput;
+import com.google.common.io.ByteStreams;
+import io.opencensus.implcore.internal.VarInt;
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for deserializing tags with {@link SerializationUtils} and {@link
+ * TagContextBinarySerializerImpl}.
+ */
+@RunWith(JUnit4.class)
+public class TagContextDeserializationTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private final TagsComponent tagsComponent = new TagsComponentImplBase();
+  private final TagContextBinarySerializer serializer =
+      tagsComponent.getTagPropagationComponent().getBinarySerializer();
+  private final Tagger tagger = tagsComponent.getTagger();
+
+  @Test
+  public void testConstants() {
+    // Refer to the JavaDoc on SerializationUtils for the definitions on these constants.
+    assertThat(SerializationUtils.VERSION_ID).isEqualTo(0);
+    assertThat(SerializationUtils.TAG_FIELD_ID).isEqualTo(0);
+    assertThat(SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT).isEqualTo(8192);
+  }
+
+  @Test
+  public void testDeserializeNoTags() throws TagContextDeserializationException {
+    TagContext expected = tagger.empty();
+    TagContext actual =
+        serializer.fromByteArray(
+            new byte[] {SerializationUtils.VERSION_ID}); // One byte that represents Version ID.
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testDeserializeEmptyByteArrayThrowException()
+      throws TagContextDeserializationException {
+    thrown.expect(TagContextDeserializationException.class);
+    thrown.expectMessage("Input byte[] can not be empty.");
+    serializer.fromByteArray(new byte[0]);
+  }
+
+  @Test
+  public void testDeserializeTooLargeByteArrayThrowException()
+      throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+    for (int i = 0; i < SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT / 8 - 1; i++) {
+      // Each tag will be with format {key : "0123", value : "0123"}, so the length of it is 8.
+      String str;
+      if (i < 10) {
+        str = "000" + i;
+      } else if (i < 100) {
+        str = "00" + i;
+      } else if (i < 1000) {
+        str = "0" + i;
+      } else {
+        str = String.valueOf(i);
+      }
+      encodeTagToOutput(str, str, output);
+    }
+    // The last tag will be of size 9, so the total size of the TagContext (8193) will be one byte
+    // more than limit.
+    encodeTagToOutput("last", "last1", output);
+
+    byte[] bytes = output.toByteArray();
+    thrown.expect(TagContextDeserializationException.class);
+    thrown.expectMessage("Size of TagContext exceeds the maximum serialized size ");
+    serializer.fromByteArray(bytes);
+  }
+
+  // Deserializing this input should cause an error, even though it represents a relatively small
+  // TagContext.
+  @Test
+  public void testDeserializeTooLargeByteArrayThrowException_WithDuplicateTagKeys()
+      throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+    for (int i = 0; i < SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT / 8 - 1; i++) {
+      // Each tag will be with format {key : "key_", value : "0123"}, so the length of it is 8.
+      String str;
+      if (i < 10) {
+        str = "000" + i;
+      } else if (i < 100) {
+        str = "00" + i;
+      } else if (i < 1000) {
+        str = "0" + i;
+      } else {
+        str = String.valueOf(i);
+      }
+      encodeTagToOutput("key_", str, output);
+    }
+    // The last tag will be of size 9, so the total size of the TagContext (8193) will be one byte
+    // more than limit.
+    encodeTagToOutput("key_", "last1", output);
+
+    byte[] bytes = output.toByteArray();
+    thrown.expect(TagContextDeserializationException.class);
+    thrown.expectMessage("Size of TagContext exceeds the maximum serialized size ");
+    serializer.fromByteArray(bytes);
+  }
+
+  @Test
+  public void testDeserializeInvalidTagKey() throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+
+    // Encode an invalid tag key and a valid tag value:
+    encodeTagToOutput("\2key", "value", output);
+    final byte[] bytes = output.toByteArray();
+
+    thrown.expect(TagContextDeserializationException.class);
+    thrown.expectMessage("Invalid tag key: \2key");
+    serializer.fromByteArray(bytes);
+  }
+
+  @Test
+  public void testDeserializeInvalidTagValue() throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+
+    // Encode a valid tag key and an invalid tag value:
+    encodeTagToOutput("my key", "val\3", output);
+    final byte[] bytes = output.toByteArray();
+
+    thrown.expect(TagContextDeserializationException.class);
+    thrown.expectMessage("Invalid tag value for key TagKey{name=my key}: val\3");
+    serializer.fromByteArray(bytes);
+  }
+
+  @Test
+  public void testDeserializeOneTag() throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+    encodeTagToOutput("Key", "Value", output);
+    TagContext expected =
+        tagger.emptyBuilder().put(TagKey.create("Key"), TagValue.create("Value")).build();
+    assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+  }
+
+  @Test
+  public void testDeserializeMultipleTags() throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+    encodeTagToOutput("Key1", "Value1", output);
+    encodeTagToOutput("Key2", "Value2", output);
+    TagContext expected =
+        tagger
+            .emptyBuilder()
+            .put(TagKey.create("Key1"), TagValue.create("Value1"))
+            .put(TagKey.create("Key2"), TagValue.create("Value2"))
+            .build();
+    assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+  }
+
+  @Test
+  public void testDeserializeDuplicateKeys() throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+    encodeTagToOutput("Key1", "Value1", output);
+    encodeTagToOutput("Key1", "Value2", output);
+    TagContext expected =
+        tagger.emptyBuilder().put(TagKey.create("Key1"), TagValue.create("Value2")).build();
+    assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+  }
+
+  @Test
+  public void testDeserializeNonConsecutiveDuplicateKeys()
+      throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+    encodeTagToOutput("Key1", "Value1", output);
+    encodeTagToOutput("Key2", "Value2", output);
+    encodeTagToOutput("Key3", "Value3", output);
+    encodeTagToOutput("Key1", "Value4", output);
+    encodeTagToOutput("Key2", "Value5", output);
+    TagContext expected =
+        tagger
+            .emptyBuilder()
+            .put(TagKey.create("Key1"), TagValue.create("Value4"))
+            .put(TagKey.create("Key2"), TagValue.create("Value5"))
+            .put(TagKey.create("Key3"), TagValue.create("Value3"))
+            .build();
+    assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+  }
+
+  @Test
+  public void testDeserializeDuplicateTags() throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+    encodeTagToOutput("Key1", "Value1", output);
+    encodeTagToOutput("Key1", "Value1", output);
+    TagContext expected =
+        tagger.emptyBuilder().put(TagKey.create("Key1"), TagValue.create("Value1")).build();
+    assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+  }
+
+  @Test
+  public void testDeserializeNonConsecutiveDuplicateTags()
+      throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+    encodeTagToOutput("Key1", "Value1", output);
+    encodeTagToOutput("Key2", "Value2", output);
+    encodeTagToOutput("Key3", "Value3", output);
+    encodeTagToOutput("Key1", "Value1", output);
+    encodeTagToOutput("Key2", "Value2", output);
+    TagContext expected =
+        tagger
+            .emptyBuilder()
+            .put(TagKey.create("Key1"), TagValue.create("Value1"))
+            .put(TagKey.create("Key2"), TagValue.create("Value2"))
+            .put(TagKey.create("Key3"), TagValue.create("Value3"))
+            .build();
+    assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+  }
+
+  @Test
+  public void stopParsingAtUnknownField() throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+    encodeTagToOutput("Key1", "Value1", output);
+    encodeTagToOutput("Key2", "Value2", output);
+
+    // Write unknown field ID 1.
+    output.write(1);
+    output.write(new byte[] {1, 2, 3, 4});
+
+    encodeTagToOutput("Key3", "Value3", output);
+
+    // key 3 should not be included
+    TagContext expected =
+        tagger
+            .emptyBuilder()
+            .put(TagKey.create("Key1"), TagValue.create("Value1"))
+            .put(TagKey.create("Key2"), TagValue.create("Value2"))
+            .build();
+    assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+  }
+
+  @Test
+  public void stopParsingAtUnknownTagAtStart() throws TagContextDeserializationException {
+    ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    output.write(SerializationUtils.VERSION_ID);
+
+    // Write unknown field ID 1.
+    output.write(1);
+    output.write(new byte[] {1, 2, 3, 4});
+
+    encodeTagToOutput("Key", "Value", output);
+    assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(tagger.empty());
+  }
+
+  @Test
+  public void testDeserializeWrongFormat() throws TagContextDeserializationException {
+    // encoded tags should follow the format <version_id>(<tag_field_id><tag_encoding>)*
+    thrown.expect(TagContextDeserializationException.class);
+    serializer.fromByteArray(new byte[3]);
+  }
+
+  @Test
+  public void testDeserializeWrongVersionId() throws TagContextDeserializationException {
+    thrown.expect(TagContextDeserializationException.class);
+    thrown.expectMessage("Wrong Version ID: 1. Currently supports version up to: 0");
+    serializer.fromByteArray(new byte[] {(byte) (SerializationUtils.VERSION_ID + 1)});
+  }
+
+  @Test
+  public void testDeserializeNegativeVersionId() throws TagContextDeserializationException {
+    thrown.expect(TagContextDeserializationException.class);
+    thrown.expectMessage("Wrong Version ID: -1. Currently supports version up to: 0");
+    serializer.fromByteArray(new byte[] {(byte) -1});
+  }
+
+  //     <tag_encoding> ==
+  //       <tag_key_len><tag_key><tag_val_len><tag_val>
+  //         <tag_key_len> == varint encoded integer
+  //         <tag_key> == tag_key_len bytes comprising tag key name
+  //         <tag_val_len> == varint encoded integer
+  //         <tag_val> == tag_val_len bytes comprising UTF-8 string
+  private static void encodeTagToOutput(String key, String value, ByteArrayDataOutput output) {
+    output.write(SerializationUtils.TAG_FIELD_ID);
+    encodeString(key, output);
+    encodeString(value, output);
+  }
+
+  private static void encodeString(String input, ByteArrayDataOutput output) {
+    int length = input.length();
+    byte[] bytes = new byte[VarInt.varIntSize(length)];
+    VarInt.putVarInt(length, bytes, 0);
+    output.write(bytes);
+    output.write(input.getBytes(Charsets.UTF_8));
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextRoundtripTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextRoundtripTest.java
new file mode 100644
index 0000000..1b1aa04
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextRoundtripTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.implcore.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for roundtrip serialization with {@link TagContextBinarySerializerImpl}. */
+@RunWith(JUnit4.class)
+public class TagContextRoundtripTest {
+
+  private static final TagKey K1 = TagKey.create("k1");
+  private static final TagKey K2 = TagKey.create("k2");
+  private static final TagKey K3 = TagKey.create("k3");
+
+  private static final TagValue V_EMPTY = TagValue.create("");
+  private static final TagValue V1 = TagValue.create("v1");
+  private static final TagValue V2 = TagValue.create("v2");
+  private static final TagValue V3 = TagValue.create("v3");
+
+  private final TagsComponent tagsComponent = new TagsComponentImplBase();
+  private final TagContextBinarySerializer serializer =
+      tagsComponent.getTagPropagationComponent().getBinarySerializer();
+  private final Tagger tagger = tagsComponent.getTagger();
+
+  @Test
+  public void testRoundtripSerialization_NormalTagContext() throws Exception {
+    testRoundtripSerialization(tagger.empty());
+    testRoundtripSerialization(tagger.emptyBuilder().put(K1, V1).build());
+    testRoundtripSerialization(tagger.emptyBuilder().put(K1, V1).put(K2, V2).put(K3, V3).build());
+    testRoundtripSerialization(tagger.emptyBuilder().put(K1, V_EMPTY).build());
+  }
+
+  @Test
+  public void testRoundtrip_TagContextWithMaximumSize() throws Exception {
+    TagContextBuilder builder = tagger.emptyBuilder();
+    for (int i = 0; i < SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT / 8; i++) {
+      // Each tag will be with format {key : "0123", value : "0123"}, so the length of it is 8.
+      // Add 1024 tags, the total size should just be 8192.
+      String str;
+      if (i < 10) {
+        str = "000" + i;
+      } else if (i < 100) {
+        str = "00" + i;
+      } else if (i < 1000) {
+        str = "0" + i;
+      } else {
+        str = "" + i;
+      }
+      builder.put(TagKey.create(str), TagValue.create(str));
+    }
+    testRoundtripSerialization(builder.build());
+  }
+
+  private void testRoundtripSerialization(TagContext expected) throws Exception {
+    byte[] bytes = serializer.toByteArray(expected);
+    TagContext actual = serializer.fromByteArray(bytes);
+    assertThat(actual).isEqualTo(expected);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextSerializationTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextSerializationTest.java
new file mode 100644
index 0000000..ed68fe3
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextSerializationTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2016-17, 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.implcore.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Collections2;
+import io.opencensus.implcore.internal.VarInt;
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextSerializationException;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for serializing tags with {@link SerializationUtils} and {@link
+ * TagContextBinarySerializerImpl}.
+ */
+@RunWith(JUnit4.class)
+public class TagContextSerializationTest {
+
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+
+  private static final TagKey K1 = TagKey.create("k1");
+  private static final TagKey K2 = TagKey.create("k2");
+  private static final TagKey K3 = TagKey.create("k3");
+  private static final TagKey K4 = TagKey.create("k4");
+
+  private static final TagValue V1 = TagValue.create("v1");
+  private static final TagValue V2 = TagValue.create("v2");
+  private static final TagValue V3 = TagValue.create("v3");
+  private static final TagValue V4 = TagValue.create("v4");
+
+  private static final Tag T1 = Tag.create(K1, V1);
+  private static final Tag T2 = Tag.create(K2, V2);
+  private static final Tag T3 = Tag.create(K3, V3);
+  private static final Tag T4 = Tag.create(K4, V4);
+
+  private final TagsComponent tagsComponent = new TagsComponentImplBase();
+  private final TagContextBinarySerializer serializer =
+      tagsComponent.getTagPropagationComponent().getBinarySerializer();
+  private final Tagger tagger = tagsComponent.getTagger();
+
+  @Test
+  public void testSerializeDefault() throws Exception {
+    testSerialize();
+  }
+
+  @Test
+  public void testSerializeWithOneTag() throws Exception {
+    testSerialize(T1);
+  }
+
+  @Test
+  public void testSerializeWithMultipleTags() throws Exception {
+    testSerialize(T1, T2, T3, T4);
+  }
+
+  @Test
+  public void testSerializeTooLargeTagContext() throws TagContextSerializationException {
+    TagContextBuilder builder = tagger.emptyBuilder();
+    for (int i = 0; i < SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT / 8 - 1; i++) {
+      // Each tag will be with format {key : "0123", value : "0123"}, so the length of it is 8.
+      String str;
+      if (i < 10) {
+        str = "000" + i;
+      } else if (i < 100) {
+        str = "00" + i;
+      } else if (i < 1000) {
+        str = "0" + i;
+      } else {
+        str = String.valueOf(i);
+      }
+      builder.put(TagKey.create(str), TagValue.create(str));
+    }
+    // The last tag will be of size 9, so the total size of the TagContext (8193) will be one byte
+    // more than limit.
+    builder.put(TagKey.create("last"), TagValue.create("last1"));
+
+    TagContext tagContext = builder.build();
+    thrown.expect(TagContextSerializationException.class);
+    thrown.expectMessage("Size of TagContext exceeds the maximum serialized size ");
+    serializer.toByteArray(tagContext);
+  }
+
+  private void testSerialize(Tag... tags) throws IOException, TagContextSerializationException {
+    TagContextBuilder builder = tagger.emptyBuilder();
+    for (Tag tag : tags) {
+      builder.put(tag.getKey(), tag.getValue());
+    }
+
+    byte[] actual = serializer.toByteArray(builder.build());
+
+    Collection<List<Tag>> tagPermutation = Collections2.permutations(Arrays.asList(tags));
+    Set<String> possibleOutputs = new HashSet<String>();
+    for (List<Tag> list : tagPermutation) {
+      ByteArrayOutputStream expected = new ByteArrayOutputStream();
+      expected.write(SerializationUtils.VERSION_ID);
+      for (Tag tag : list) {
+        expected.write(SerializationUtils.TAG_FIELD_ID);
+        encodeString(tag.getKey().getName(), expected);
+        encodeString(tag.getValue().asString(), expected);
+      }
+      possibleOutputs.add(new String(expected.toByteArray(), Charsets.UTF_8));
+    }
+
+    assertThat(possibleOutputs).contains(new String(actual, Charsets.UTF_8));
+  }
+
+  private static void encodeString(String input, ByteArrayOutputStream byteArrayOutputStream)
+      throws IOException {
+    VarInt.putVarInt(input.length(), byteArrayOutputStream);
+    byteArrayOutputStream.write(input.getBytes(Charsets.UTF_8));
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/NoRecordEventsSpanImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/NoRecordEventsSpanImplTest.java
new file mode 100644
index 0000000..c576860
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/NoRecordEventsSpanImplTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018, 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.implcore.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.NetworkEvent;
+import io.opencensus.trace.Span.Options;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link NoRecordEventsSpanImpl}. */
+@RunWith(JUnit4.class)
+public class NoRecordEventsSpanImplTest {
+  private final Random random = new Random(1234);
+  private final SpanContext spanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random),
+          SpanId.generateRandomId(random),
+          TraceOptions.DEFAULT,
+          Tracestate.builder().build());
+  private final NoRecordEventsSpanImpl noRecordEventsSpan =
+      NoRecordEventsSpanImpl.create(spanContext);
+
+  @Test
+  public void propagatesSpanContext() {
+    assertThat(noRecordEventsSpan.getContext()).isEqualTo(spanContext);
+  }
+
+  @Test
+  public void hasNoRecordEventsOption() {
+    assertThat(noRecordEventsSpan.getOptions()).doesNotContain(Options.RECORD_EVENTS);
+  }
+
+  @Test
+  public void doNotCrash() {
+    Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+    attributes.put(
+        "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+    Map<String, AttributeValue> multipleAttributes = new HashMap<String, AttributeValue>();
+    multipleAttributes.put(
+        "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+    multipleAttributes.put("MyBooleanAttributeKey", AttributeValue.booleanAttributeValue(true));
+    multipleAttributes.put("MyLongAttributeKey", AttributeValue.longAttributeValue(123));
+    // Tests only that all the methods are not crashing/throwing errors.
+    noRecordEventsSpan.putAttribute(
+        "MyStringAttributeKey2", AttributeValue.stringAttributeValue("MyStringAttributeValue2"));
+    noRecordEventsSpan.addAttributes(attributes);
+    noRecordEventsSpan.addAttributes(multipleAttributes);
+    noRecordEventsSpan.addAnnotation("MyAnnotation");
+    noRecordEventsSpan.addAnnotation("MyAnnotation", attributes);
+    noRecordEventsSpan.addAnnotation("MyAnnotation", multipleAttributes);
+    noRecordEventsSpan.addAnnotation(Annotation.fromDescription("MyAnnotation"));
+    noRecordEventsSpan.addNetworkEvent(NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).build());
+    noRecordEventsSpan.addMessageEvent(MessageEvent.builder(MessageEvent.Type.SENT, 1L).build());
+    noRecordEventsSpan.addLink(
+        Link.fromSpanContext(SpanContext.INVALID, Link.Type.CHILD_LINKED_SPAN));
+    noRecordEventsSpan.setStatus(Status.OK);
+    noRecordEventsSpan.end(EndSpanOptions.DEFAULT);
+    noRecordEventsSpan.end();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/RecordEventsSpanImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/RecordEventsSpanImplTest.java
new file mode 100644
index 0000000..b293a22
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/RecordEventsSpanImplTest.java
@@ -0,0 +1,594 @@
+/*
+ * 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.implcore.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.NetworkEvent;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link RecordEventsSpanImpl}. */
+@RunWith(JUnit4.class)
+public class RecordEventsSpanImplTest {
+  private static final String SPAN_NAME = "MySpanName";
+  private static final String ANNOTATION_DESCRIPTION = "MyAnnotation";
+  private final Random random = new Random(1234);
+  private final SpanContext spanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+  private final SpanId parentSpanId = SpanId.generateRandomId(random);
+  private final Timestamp timestamp = Timestamp.create(1234, 5678);
+  private final TestClock testClock = TestClock.create(timestamp);
+  private final TimestampConverter timestampConverter = TimestampConverter.now(testClock);
+  private final Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+  private final Map<String, AttributeValue> expectedAttributes =
+      new HashMap<String, AttributeValue>();
+  @Mock private StartEndHandler startEndHandler;
+  @Rule public final ExpectedException exception = ExpectedException.none();
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    attributes.put(
+        "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+    attributes.put("MyLongAttributeKey", AttributeValue.longAttributeValue(123L));
+    attributes.put("MyBooleanAttributeKey", AttributeValue.booleanAttributeValue(false));
+    expectedAttributes.putAll(attributes);
+    expectedAttributes.put(
+        "MySingleStringAttributeKey",
+        AttributeValue.stringAttributeValue("MySingleStringAttributeValue"));
+  }
+
+  @Test
+  public void noEventsRecordedAfterEnd() {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    span.end();
+    // Check that adding trace events after Span#end() does not throw any exception and are not
+    // recorded.
+    span.putAttributes(attributes);
+    span.putAttribute(
+        "MySingleStringAttributeKey",
+        AttributeValue.stringAttributeValue("MySingleStringAttributeValue"));
+    span.addAnnotation(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+    span.addAnnotation(ANNOTATION_DESCRIPTION, attributes);
+    span.addNetworkEvent(
+        NetworkEvent.builder(NetworkEvent.Type.RECV, 1).setUncompressedMessageSize(3).build());
+    span.addLink(Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN));
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getStartTimestamp()).isEqualTo(timestamp);
+    assertThat(spanData.getAttributes().getAttributeMap()).isEmpty();
+    assertThat(spanData.getAnnotations().getEvents()).isEmpty();
+    assertThat(spanData.getNetworkEvents().getEvents()).isEmpty();
+    assertThat(spanData.getLinks().getLinks()).isEmpty();
+    assertThat(spanData.getStatus()).isEqualTo(Status.OK);
+    assertThat(spanData.getEndTimestamp()).isEqualTo(timestamp);
+  }
+
+  @Test
+  public void deprecatedAddAttributesStillWorks() {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    span.addAttributes(attributes);
+    span.end();
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getAttributes().getAttributeMap()).isEqualTo(attributes);
+  }
+
+  @Test
+  public void toSpanData_ActiveSpan() {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            true,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    Mockito.verify(startEndHandler, Mockito.times(1)).onStart(span);
+    span.putAttribute(
+        "MySingleStringAttributeKey",
+        AttributeValue.stringAttributeValue("MySingleStringAttributeValue"));
+    span.putAttributes(attributes);
+    testClock.advanceTime(Duration.create(0, 100));
+    span.addAnnotation(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+    testClock.advanceTime(Duration.create(0, 100));
+    span.addAnnotation(ANNOTATION_DESCRIPTION, attributes);
+    testClock.advanceTime(Duration.create(0, 100));
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.RECV, 1).setUncompressedMessageSize(3).build();
+    span.addNetworkEvent(networkEvent);
+    testClock.advanceTime(Duration.create(0, 100));
+    Link link = Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN);
+    span.addLink(link);
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getContext()).isEqualTo(spanContext);
+    assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+    assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+    assertThat(spanData.getHasRemoteParent()).isTrue();
+    assertThat(spanData.getAttributes().getDroppedAttributesCount()).isEqualTo(0);
+    assertThat(spanData.getAttributes().getAttributeMap()).isEqualTo(expectedAttributes);
+    assertThat(spanData.getAnnotations().getDroppedEventsCount()).isEqualTo(0);
+    assertThat(spanData.getAnnotations().getEvents().size()).isEqualTo(2);
+    assertThat(spanData.getAnnotations().getEvents().get(0).getTimestamp())
+        .isEqualTo(timestamp.addNanos(100));
+    assertThat(spanData.getAnnotations().getEvents().get(0).getEvent())
+        .isEqualTo(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+    assertThat(spanData.getAnnotations().getEvents().get(1).getTimestamp())
+        .isEqualTo(timestamp.addNanos(200));
+    assertThat(spanData.getAnnotations().getEvents().get(1).getEvent())
+        .isEqualTo(Annotation.fromDescriptionAndAttributes(ANNOTATION_DESCRIPTION, attributes));
+    assertThat(spanData.getNetworkEvents().getDroppedEventsCount()).isEqualTo(0);
+    assertThat(spanData.getNetworkEvents().getEvents().size()).isEqualTo(1);
+    assertThat(spanData.getNetworkEvents().getEvents().get(0).getTimestamp())
+        .isEqualTo(timestamp.addNanos(300));
+    assertThat(spanData.getNetworkEvents().getEvents().get(0).getEvent()).isEqualTo(networkEvent);
+    assertThat(spanData.getLinks().getDroppedLinksCount()).isEqualTo(0);
+    assertThat(spanData.getLinks().getLinks().size()).isEqualTo(1);
+    assertThat(spanData.getLinks().getLinks().get(0)).isEqualTo(link);
+    assertThat(spanData.getStartTimestamp()).isEqualTo(timestamp);
+    assertThat(spanData.getStatus()).isNull();
+    assertThat(spanData.getEndTimestamp()).isNull();
+  }
+
+  @Test
+  public void toSpanData_EndedSpan() {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    Mockito.verify(startEndHandler, Mockito.times(1)).onStart(span);
+    span.putAttribute(
+        "MySingleStringAttributeKey",
+        AttributeValue.stringAttributeValue("MySingleStringAttributeValue"));
+    span.putAttributes(attributes);
+    testClock.advanceTime(Duration.create(0, 100));
+    span.addAnnotation(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+    testClock.advanceTime(Duration.create(0, 100));
+    span.addAnnotation(ANNOTATION_DESCRIPTION, attributes);
+    testClock.advanceTime(Duration.create(0, 100));
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.RECV, 1).setUncompressedMessageSize(3).build();
+    span.addNetworkEvent(networkEvent);
+    Link link = Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN);
+    span.addLink(link);
+    testClock.advanceTime(Duration.create(0, 100));
+    span.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+    Mockito.verify(startEndHandler, Mockito.times(1)).onEnd(span);
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getContext()).isEqualTo(spanContext);
+    assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+    assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+    assertThat(spanData.getHasRemoteParent()).isFalse();
+    assertThat(spanData.getAttributes().getDroppedAttributesCount()).isEqualTo(0);
+    assertThat(spanData.getAttributes().getAttributeMap()).isEqualTo(expectedAttributes);
+    assertThat(spanData.getAnnotations().getDroppedEventsCount()).isEqualTo(0);
+    assertThat(spanData.getAnnotations().getEvents().size()).isEqualTo(2);
+    assertThat(spanData.getAnnotations().getEvents().get(0).getTimestamp())
+        .isEqualTo(timestamp.addNanos(100));
+    assertThat(spanData.getAnnotations().getEvents().get(0).getEvent())
+        .isEqualTo(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+    assertThat(spanData.getAnnotations().getEvents().get(1).getTimestamp())
+        .isEqualTo(timestamp.addNanos(200));
+    assertThat(spanData.getAnnotations().getEvents().get(1).getEvent())
+        .isEqualTo(Annotation.fromDescriptionAndAttributes(ANNOTATION_DESCRIPTION, attributes));
+    assertThat(spanData.getNetworkEvents().getDroppedEventsCount()).isEqualTo(0);
+    assertThat(spanData.getNetworkEvents().getEvents().size()).isEqualTo(1);
+    assertThat(spanData.getNetworkEvents().getEvents().get(0).getTimestamp())
+        .isEqualTo(timestamp.addNanos(300));
+    assertThat(spanData.getNetworkEvents().getEvents().get(0).getEvent()).isEqualTo(networkEvent);
+    assertThat(spanData.getLinks().getDroppedLinksCount()).isEqualTo(0);
+    assertThat(spanData.getLinks().getLinks().size()).isEqualTo(1);
+    assertThat(spanData.getLinks().getLinks().get(0)).isEqualTo(link);
+    assertThat(spanData.getStartTimestamp()).isEqualTo(timestamp);
+    assertThat(spanData.getStatus()).isEqualTo(Status.CANCELLED);
+    assertThat(spanData.getEndTimestamp()).isEqualTo(timestamp.addNanos(400));
+  }
+
+  @Test
+  public void status_ViaSetStatus() {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    Mockito.verify(startEndHandler, Mockito.times(1)).onStart(span);
+    testClock.advanceTime(Duration.create(0, 100));
+    assertThat(span.getStatus()).isEqualTo(Status.OK);
+    span.setStatus(Status.CANCELLED);
+    assertThat(span.getStatus()).isEqualTo(Status.CANCELLED);
+    span.end();
+    assertThat(span.getStatus()).isEqualTo(Status.CANCELLED);
+  }
+
+  @Test
+  public void status_ViaEndSpanOptions() {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    Mockito.verify(startEndHandler, Mockito.times(1)).onStart(span);
+    testClock.advanceTime(Duration.create(0, 100));
+    assertThat(span.getStatus()).isEqualTo(Status.OK);
+    span.setStatus(Status.CANCELLED);
+    assertThat(span.getStatus()).isEqualTo(Status.CANCELLED);
+    span.end(EndSpanOptions.builder().setStatus(Status.ABORTED).build());
+    assertThat(span.getStatus()).isEqualTo(Status.ABORTED);
+  }
+
+  @Test
+  public void droppingAttributes() {
+    final int maxNumberOfAttributes = 8;
+    TraceParams traceParams =
+        TraceParams.DEFAULT.toBuilder().setMaxNumberOfAttributes(maxNumberOfAttributes).build();
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            traceParams,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    for (int i = 0; i < 2 * maxNumberOfAttributes; i++) {
+      Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+      attributes.put("MyStringAttributeKey" + i, AttributeValue.longAttributeValue(i));
+      span.putAttributes(attributes);
+    }
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getAttributes().getDroppedAttributesCount())
+        .isEqualTo(maxNumberOfAttributes);
+    assertThat(spanData.getAttributes().getAttributeMap().size()).isEqualTo(maxNumberOfAttributes);
+    for (int i = 0; i < maxNumberOfAttributes; i++) {
+      assertThat(
+              spanData
+                  .getAttributes()
+                  .getAttributeMap()
+                  .get("MyStringAttributeKey" + (i + maxNumberOfAttributes)))
+          .isEqualTo(AttributeValue.longAttributeValue(i + maxNumberOfAttributes));
+    }
+    span.end();
+    spanData = span.toSpanData();
+    assertThat(spanData.getAttributes().getDroppedAttributesCount())
+        .isEqualTo(maxNumberOfAttributes);
+    assertThat(spanData.getAttributes().getAttributeMap().size()).isEqualTo(maxNumberOfAttributes);
+    for (int i = 0; i < maxNumberOfAttributes; i++) {
+      assertThat(
+              spanData
+                  .getAttributes()
+                  .getAttributeMap()
+                  .get("MyStringAttributeKey" + (i + maxNumberOfAttributes)))
+          .isEqualTo(AttributeValue.longAttributeValue(i + maxNumberOfAttributes));
+    }
+  }
+
+  @Test
+  public void droppingAndAddingAttributes() {
+    final int maxNumberOfAttributes = 8;
+    TraceParams traceParams =
+        TraceParams.DEFAULT.toBuilder().setMaxNumberOfAttributes(maxNumberOfAttributes).build();
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            traceParams,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    for (int i = 0; i < 2 * maxNumberOfAttributes; i++) {
+      Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+      attributes.put("MyStringAttributeKey" + i, AttributeValue.longAttributeValue(i));
+      span.putAttributes(attributes);
+    }
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getAttributes().getDroppedAttributesCount())
+        .isEqualTo(maxNumberOfAttributes);
+    assertThat(spanData.getAttributes().getAttributeMap().size()).isEqualTo(maxNumberOfAttributes);
+    for (int i = 0; i < maxNumberOfAttributes; i++) {
+      assertThat(
+              spanData
+                  .getAttributes()
+                  .getAttributeMap()
+                  .get("MyStringAttributeKey" + (i + maxNumberOfAttributes)))
+          .isEqualTo(AttributeValue.longAttributeValue(i + maxNumberOfAttributes));
+    }
+    for (int i = 0; i < maxNumberOfAttributes / 2; i++) {
+      Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+      attributes.put("MyStringAttributeKey" + i, AttributeValue.longAttributeValue(i));
+      span.putAttributes(attributes);
+    }
+    spanData = span.toSpanData();
+    assertThat(spanData.getAttributes().getDroppedAttributesCount())
+        .isEqualTo(maxNumberOfAttributes * 3 / 2);
+    assertThat(spanData.getAttributes().getAttributeMap().size()).isEqualTo(maxNumberOfAttributes);
+    // Test that we still have in the attributes map the latest maxNumberOfAttributes / 2 entries.
+    for (int i = 0; i < maxNumberOfAttributes / 2; i++) {
+      assertThat(
+              spanData
+                  .getAttributes()
+                  .getAttributeMap()
+                  .get("MyStringAttributeKey" + (i + maxNumberOfAttributes * 3 / 2)))
+          .isEqualTo(AttributeValue.longAttributeValue(i + maxNumberOfAttributes * 3 / 2));
+    }
+    // Test that we have the newest re-added initial entries.
+    for (int i = 0; i < maxNumberOfAttributes / 2; i++) {
+      assertThat(spanData.getAttributes().getAttributeMap().get("MyStringAttributeKey" + i))
+          .isEqualTo(AttributeValue.longAttributeValue(i));
+    }
+  }
+
+  @Test
+  public void droppingAnnotations() {
+    final int maxNumberOfAnnotations = 8;
+    TraceParams traceParams =
+        TraceParams.DEFAULT.toBuilder().setMaxNumberOfAnnotations(maxNumberOfAnnotations).build();
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            traceParams,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    Annotation annotation = Annotation.fromDescription(ANNOTATION_DESCRIPTION);
+    for (int i = 0; i < 2 * maxNumberOfAnnotations; i++) {
+      span.addAnnotation(annotation);
+      testClock.advanceTime(Duration.create(0, 100));
+    }
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getAnnotations().getDroppedEventsCount()).isEqualTo(maxNumberOfAnnotations);
+    assertThat(spanData.getAnnotations().getEvents().size()).isEqualTo(maxNumberOfAnnotations);
+    for (int i = 0; i < maxNumberOfAnnotations; i++) {
+      assertThat(spanData.getAnnotations().getEvents().get(i).getTimestamp())
+          .isEqualTo(timestamp.addNanos(100L * (maxNumberOfAnnotations + i)));
+      assertThat(spanData.getAnnotations().getEvents().get(i).getEvent()).isEqualTo(annotation);
+    }
+    span.end();
+    spanData = span.toSpanData();
+    assertThat(spanData.getAnnotations().getDroppedEventsCount()).isEqualTo(maxNumberOfAnnotations);
+    assertThat(spanData.getAnnotations().getEvents().size()).isEqualTo(maxNumberOfAnnotations);
+    for (int i = 0; i < maxNumberOfAnnotations; i++) {
+      assertThat(spanData.getAnnotations().getEvents().get(i).getTimestamp())
+          .isEqualTo(timestamp.addNanos(100L * (maxNumberOfAnnotations + i)));
+      assertThat(spanData.getAnnotations().getEvents().get(i).getEvent()).isEqualTo(annotation);
+    }
+  }
+
+  @Test
+  public void droppingNetworkEvents() {
+    final int maxNumberOfNetworkEvents = 8;
+    TraceParams traceParams =
+        TraceParams.DEFAULT
+            .toBuilder()
+            .setMaxNumberOfNetworkEvents(maxNumberOfNetworkEvents)
+            .build();
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            traceParams,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    NetworkEvent networkEvent =
+        NetworkEvent.builder(NetworkEvent.Type.RECV, 1).setUncompressedMessageSize(3).build();
+    for (int i = 0; i < 2 * maxNumberOfNetworkEvents; i++) {
+      span.addNetworkEvent(networkEvent);
+      testClock.advanceTime(Duration.create(0, 100));
+    }
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getNetworkEvents().getDroppedEventsCount())
+        .isEqualTo(maxNumberOfNetworkEvents);
+    assertThat(spanData.getNetworkEvents().getEvents().size()).isEqualTo(maxNumberOfNetworkEvents);
+    for (int i = 0; i < maxNumberOfNetworkEvents; i++) {
+      assertThat(spanData.getNetworkEvents().getEvents().get(i).getTimestamp())
+          .isEqualTo(timestamp.addNanos(100L * (maxNumberOfNetworkEvents + i)));
+      assertThat(spanData.getNetworkEvents().getEvents().get(i).getEvent()).isEqualTo(networkEvent);
+    }
+    span.end();
+    spanData = span.toSpanData();
+    assertThat(spanData.getNetworkEvents().getDroppedEventsCount())
+        .isEqualTo(maxNumberOfNetworkEvents);
+    assertThat(spanData.getNetworkEvents().getEvents().size()).isEqualTo(maxNumberOfNetworkEvents);
+    for (int i = 0; i < maxNumberOfNetworkEvents; i++) {
+      assertThat(spanData.getNetworkEvents().getEvents().get(i).getTimestamp())
+          .isEqualTo(timestamp.addNanos(100L * (maxNumberOfNetworkEvents + i)));
+      assertThat(spanData.getNetworkEvents().getEvents().get(i).getEvent()).isEqualTo(networkEvent);
+    }
+  }
+
+  @Test
+  public void droppingLinks() {
+    final int maxNumberOfLinks = 8;
+    TraceParams traceParams =
+        TraceParams.DEFAULT.toBuilder().setMaxNumberOfLinks(maxNumberOfLinks).build();
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            traceParams,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    Link link = Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN);
+    for (int i = 0; i < 2 * maxNumberOfLinks; i++) {
+      span.addLink(link);
+    }
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getLinks().getDroppedLinksCount()).isEqualTo(maxNumberOfLinks);
+    assertThat(spanData.getLinks().getLinks().size()).isEqualTo(maxNumberOfLinks);
+    for (int i = 0; i < maxNumberOfLinks; i++) {
+      assertThat(spanData.getLinks().getLinks().get(i)).isEqualTo(link);
+    }
+    span.end();
+    spanData = span.toSpanData();
+    assertThat(spanData.getLinks().getDroppedLinksCount()).isEqualTo(maxNumberOfLinks);
+    assertThat(spanData.getLinks().getLinks().size()).isEqualTo(maxNumberOfLinks);
+    for (int i = 0; i < maxNumberOfLinks; i++) {
+      assertThat(spanData.getLinks().getLinks().get(i)).isEqualTo(link);
+    }
+  }
+
+  @Test
+  public void sampleToLocalSpanStore() {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    span.end(EndSpanOptions.builder().setSampleToLocalSpanStore(true).build());
+    Mockito.verify(startEndHandler, Mockito.times(1)).onEnd(span);
+    assertThat(span.getSampleToLocalSpanStore()).isTrue();
+    span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    span.end();
+    Mockito.verify(startEndHandler, Mockito.times(1)).onEnd(span);
+    assertThat(span.getSampleToLocalSpanStore()).isFalse();
+  }
+
+  @Test
+  public void sampleToLocalSpanStore_RunningSpan() {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            parentSpanId,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    exception.expect(IllegalStateException.class);
+    exception.expectMessage("Running span does not have the SampleToLocalSpanStore set.");
+    span.getSampleToLocalSpanStore();
+  }
+
+  @Test
+  public void getSpanKind() {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            Kind.SERVER,
+            parentSpanId,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    assertThat(span.getKind()).isEqualTo(Kind.SERVER);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/SpanBuilderImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/SpanBuilderImplTest.java
new file mode 100644
index 0000000..3267eac
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/SpanBuilderImplTest.java
@@ -0,0 +1,404 @@
+/*
+ * 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.implcore.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.Span.Options;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collections;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link SpanBuilderImpl}. */
+@RunWith(JUnit4.class)
+public class SpanBuilderImplTest {
+  private static final String SPAN_NAME = "MySpanName";
+  private SpanBuilderImpl.Options spanBuilderOptions;
+  private final TraceParams alwaysSampleTraceParams =
+      TraceParams.DEFAULT.toBuilder().setSampler(Samplers.alwaysSample()).build();
+  private final TestClock testClock = TestClock.create();
+  private final RandomHandler randomHandler = new FakeRandomHandler();
+  @Mock private StartEndHandler startEndHandler;
+  @Mock private TraceConfig traceConfig;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    spanBuilderOptions =
+        new SpanBuilderImpl.Options(randomHandler, startEndHandler, testClock, traceConfig);
+    when(traceConfig.getActiveTraceParams()).thenReturn(alwaysSampleTraceParams);
+  }
+
+  @Test
+  public void startSpan_CreatesTheCorrectSpanImplInstance() {
+    assertThat(
+            SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+                .setSampler(Samplers.alwaysSample())
+                .startSpan())
+        .isInstanceOf(RecordEventsSpanImpl.class);
+    assertThat(
+            SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+                .setRecordEvents(true)
+                .setSampler(Samplers.neverSample())
+                .startSpan())
+        .isInstanceOf(RecordEventsSpanImpl.class);
+    assertThat(
+            SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+                .setSampler(Samplers.neverSample())
+                .startSpan())
+        .isInstanceOf(NoRecordEventsSpanImpl.class);
+  }
+
+  @Test
+  public void setSpanKind_NotNull() {
+    RecordEventsSpanImpl span =
+        (RecordEventsSpanImpl)
+            SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+                .setSpanKind(Kind.CLIENT)
+                .setRecordEvents(true)
+                .startSpan();
+    assertThat(span.getKind()).isEqualTo(Kind.CLIENT);
+    assertThat(span.toSpanData().getKind()).isEqualTo(Kind.CLIENT);
+  }
+
+  @Test
+  public void setSpanKind_DefaultNull() {
+    RecordEventsSpanImpl span =
+        (RecordEventsSpanImpl)
+            SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+                .setRecordEvents(true)
+                .startSpan();
+    assertThat(span.getKind()).isNull();
+    assertThat(span.toSpanData().getKind()).isNull();
+  }
+
+  @Test
+  public void startSpanNullParent() {
+    RecordEventsSpanImpl span =
+        (RecordEventsSpanImpl)
+            SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+                .setRecordEvents(true)
+                .startSpan();
+    assertThat(span.getContext().isValid()).isTrue();
+    assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+    assertThat(span.getContext().getTraceOptions().isSampled()).isTrue();
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getParentSpanId()).isNull();
+    assertThat(spanData.getHasRemoteParent()).isNull();
+    assertThat(spanData.getStartTimestamp()).isEqualTo(testClock.now());
+    assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+  }
+
+  @Test
+  public void startSpanNullParentWithRecordEvents() {
+    RecordEventsSpanImpl span =
+        (RecordEventsSpanImpl)
+            SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+                .setSampler(Samplers.neverSample())
+                .setRecordEvents(true)
+                .startSpan();
+    assertThat(span.getContext().isValid()).isTrue();
+    assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+    assertThat(span.getContext().getTraceOptions().isSampled()).isFalse();
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getParentSpanId()).isNull();
+    assertThat(spanData.getHasRemoteParent()).isNull();
+  }
+
+  @Test
+  public void startSpanNullParentNoRecordOptions() {
+    Span span =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+            .setSampler(Samplers.neverSample())
+            .startSpan();
+    assertThat(span.getContext().isValid()).isTrue();
+    assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isFalse();
+    assertThat(span.getContext().getTraceOptions().isSampled()).isFalse();
+  }
+
+  @Test
+  public void startChildSpan() {
+    Span rootSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions).startSpan();
+    assertThat(rootSpan.getContext().isValid()).isTrue();
+    assertThat(rootSpan.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+    assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isTrue();
+    assertThat(((RecordEventsSpanImpl) rootSpan).toSpanData().getHasRemoteParent()).isNull();
+    Span childSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, rootSpan, spanBuilderOptions).startSpan();
+    assertThat(childSpan.getContext().isValid()).isTrue();
+    assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+    assertThat(((RecordEventsSpanImpl) childSpan).toSpanData().getParentSpanId())
+        .isEqualTo(rootSpan.getContext().getSpanId());
+    assertThat(((RecordEventsSpanImpl) childSpan).toSpanData().getHasRemoteParent()).isFalse();
+    assertThat(((RecordEventsSpanImpl) childSpan).getTimestampConverter())
+        .isEqualTo(((RecordEventsSpanImpl) rootSpan).getTimestampConverter());
+  }
+
+  @Test
+  public void startRemoteSpan_NullParent() {
+    RecordEventsSpanImpl span =
+        (RecordEventsSpanImpl)
+            SpanBuilderImpl.createWithRemoteParent(SPAN_NAME, null, spanBuilderOptions)
+                .setRecordEvents(true)
+                .startSpan();
+    assertThat(span.getContext().isValid()).isTrue();
+    assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+    assertThat(span.getContext().getTraceOptions().isSampled()).isTrue();
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getParentSpanId()).isNull();
+    assertThat(spanData.getHasRemoteParent()).isNull();
+  }
+
+  @Test
+  public void startRemoteSpanInvalidParent() {
+    RecordEventsSpanImpl span =
+        (RecordEventsSpanImpl)
+            SpanBuilderImpl.createWithRemoteParent(
+                    SPAN_NAME, SpanContext.INVALID, spanBuilderOptions)
+                .startSpan();
+    assertThat(span.getContext().isValid()).isTrue();
+    assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+    assertThat(span.getContext().getTraceOptions().isSampled()).isTrue();
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getParentSpanId()).isNull();
+    assertThat(spanData.getHasRemoteParent()).isNull();
+  }
+
+  @Test
+  public void startRemoteSpan() {
+    SpanContext spanContext =
+        SpanContext.create(
+            TraceId.generateRandomId(randomHandler.current()),
+            SpanId.generateRandomId(randomHandler.current()),
+            TraceOptions.DEFAULT);
+    RecordEventsSpanImpl span =
+        (RecordEventsSpanImpl)
+            SpanBuilderImpl.createWithRemoteParent(SPAN_NAME, spanContext, spanBuilderOptions)
+                .setRecordEvents(true)
+                .startSpan();
+    assertThat(span.getContext().isValid()).isTrue();
+    assertThat(span.getContext().getTraceId()).isEqualTo(spanContext.getTraceId());
+    assertThat(span.getContext().getTraceOptions().isSampled()).isTrue();
+    SpanData spanData = span.toSpanData();
+    assertThat(spanData.getParentSpanId()).isEqualTo(spanContext.getSpanId());
+    assertThat(spanData.getHasRemoteParent()).isTrue();
+  }
+
+  @Test
+  public void startRootSpan_WithSpecifiedSampler() {
+    // Apply given sampler before default sampler for root spans.
+    Span rootSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+            .setSampler(Samplers.neverSample())
+            .startSpan();
+    assertThat(rootSpan.getContext().isValid()).isTrue();
+    assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isFalse();
+  }
+
+  @Test
+  public void startRootSpan_WithoutSpecifiedSampler() {
+    // Apply default sampler (always true in the tests) for root spans.
+    Span rootSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions).startSpan();
+    assertThat(rootSpan.getContext().isValid()).isTrue();
+    assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isTrue();
+  }
+
+  @Test
+  public void startRemoteChildSpan_WithSpecifiedSampler() {
+    Span rootSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+            .setSampler(Samplers.alwaysSample())
+            .startSpan();
+    assertThat(rootSpan.getContext().isValid()).isTrue();
+    assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isTrue();
+    // Apply given sampler before default sampler for spans with remote parent.
+    Span childSpan =
+        SpanBuilderImpl.createWithRemoteParent(SPAN_NAME, rootSpan.getContext(), spanBuilderOptions)
+            .setSampler(Samplers.neverSample())
+            .startSpan();
+    assertThat(childSpan.getContext().isValid()).isTrue();
+    assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+    assertThat(childSpan.getContext().getTraceOptions().isSampled()).isFalse();
+  }
+
+  @Test
+  public void startRemoteChildSpan_WithoutSpecifiedSampler() {
+    Span rootSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+            .setSampler(Samplers.neverSample())
+            .startSpan();
+    assertThat(rootSpan.getContext().isValid()).isTrue();
+    assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isFalse();
+    // Apply default sampler (always true in the tests) for spans with remote parent.
+    Span childSpan =
+        SpanBuilderImpl.createWithRemoteParent(SPAN_NAME, rootSpan.getContext(), spanBuilderOptions)
+            .startSpan();
+    assertThat(childSpan.getContext().isValid()).isTrue();
+    assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+    assertThat(childSpan.getContext().getTraceOptions().isSampled()).isTrue();
+  }
+
+  @Test
+  public void startChildSpan_WithSpecifiedSampler() {
+    Span rootSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+            .setSampler(Samplers.alwaysSample())
+            .startSpan();
+    assertThat(rootSpan.getContext().isValid()).isTrue();
+    assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isTrue();
+    // Apply the given sampler for child spans.
+    Span childSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, rootSpan, spanBuilderOptions)
+            .setSampler(Samplers.neverSample())
+            .startSpan();
+    assertThat(childSpan.getContext().isValid()).isTrue();
+    assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+    assertThat(childSpan.getContext().getTraceOptions().isSampled()).isFalse();
+  }
+
+  @Test
+  public void startChildSpan_WithoutSpecifiedSampler() {
+    Span rootSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+            .setSampler(Samplers.neverSample())
+            .startSpan();
+    assertThat(rootSpan.getContext().isValid()).isTrue();
+    assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isFalse();
+    // Don't apply the default sampler (always true) for child spans.
+    Span childSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, rootSpan, spanBuilderOptions).startSpan();
+    assertThat(childSpan.getContext().isValid()).isTrue();
+    assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+    assertThat(childSpan.getContext().getTraceOptions().isSampled()).isFalse();
+  }
+
+  @Test
+  public void startChildSpan_SampledLinkedParent() {
+    Span rootSpanUnsampled =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+            .setSampler(Samplers.neverSample())
+            .startSpan();
+    assertThat(rootSpanUnsampled.getContext().getTraceOptions().isSampled()).isFalse();
+    Span rootSpanSampled =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+            .setSampler(Samplers.alwaysSample())
+            .startSpan();
+    assertThat(rootSpanSampled.getContext().getTraceOptions().isSampled()).isTrue();
+    // Sampled because the linked parent is sampled.
+    Span childSpan =
+        SpanBuilderImpl.createWithParent(SPAN_NAME, rootSpanUnsampled, spanBuilderOptions)
+            .setParentLinks(Collections.singletonList(rootSpanSampled))
+            .startSpan();
+    assertThat(childSpan.getContext().isValid()).isTrue();
+    assertThat(childSpan.getContext().getTraceId())
+        .isEqualTo(rootSpanUnsampled.getContext().getTraceId());
+    assertThat(childSpan.getContext().getTraceOptions().isSampled()).isTrue();
+  }
+
+  @Test
+  public void startRemoteChildSpan_WithProbabilitySamplerDefaultSampler() {
+    when(traceConfig.getActiveTraceParams()).thenReturn(TraceParams.DEFAULT);
+    // This traceId will not be sampled by the ProbabilitySampler because the first 8 bytes as long
+    // is not less than probability * Long.MAX_VALUE;
+    TraceId traceId =
+        TraceId.fromBytes(
+            new byte[] {
+              (byte) 0x8F,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              (byte) 0xFF,
+              0,
+              0,
+              0,
+              0,
+              0,
+              0,
+              0,
+              0
+            });
+
+    // If parent is sampled then the remote child must be sampled.
+    Span childSpan =
+        SpanBuilderImpl.createWithRemoteParent(
+                SPAN_NAME,
+                SpanContext.create(
+                    traceId,
+                    SpanId.generateRandomId(randomHandler.current()),
+                    TraceOptions.builder().setIsSampled(true).build()),
+                spanBuilderOptions)
+            .startSpan();
+    assertThat(childSpan.getContext().isValid()).isTrue();
+    assertThat(childSpan.getContext().getTraceId()).isEqualTo(traceId);
+    assertThat(childSpan.getContext().getTraceOptions().isSampled()).isTrue();
+    childSpan.end();
+
+    assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+
+    // If parent is not sampled then the remote child must be not sampled.
+    childSpan =
+        SpanBuilderImpl.createWithRemoteParent(
+                SPAN_NAME,
+                SpanContext.create(
+                    traceId,
+                    SpanId.generateRandomId(randomHandler.current()),
+                    TraceOptions.DEFAULT),
+                spanBuilderOptions)
+            .startSpan();
+    assertThat(childSpan.getContext().isValid()).isTrue();
+    assertThat(childSpan.getContext().getTraceId()).isEqualTo(traceId);
+    assertThat(childSpan.getContext().getTraceOptions().isSampled()).isFalse();
+    childSpan.end();
+  }
+
+  private static final class FakeRandomHandler extends RandomHandler {
+    private final Random random;
+
+    FakeRandomHandler() {
+      this.random = new Random(1234);
+    }
+
+    @Override
+    public Random current() {
+      return random;
+    }
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/TraceComponentImplBaseTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/TraceComponentImplBaseTest.java
new file mode 100644
index 0000000..9f46844
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/TraceComponentImplBaseTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.implcore.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.export.ExportComponentImpl;
+import io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler;
+import io.opencensus.implcore.trace.propagation.PropagationComponentImpl;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceComponentImplBase}. */
+@RunWith(JUnit4.class)
+public class TraceComponentImplBaseTest {
+  private final TraceComponentImplBase traceComponentImplBase =
+      new TraceComponentImplBase(
+          MillisClock.getInstance(), new SecureRandomHandler(), new SimpleEventQueue());
+
+  @Test
+  public void implementationOfTracer() {
+    assertThat(traceComponentImplBase.getTracer()).isInstanceOf(TracerImpl.class);
+  }
+
+  @Test
+  public void implementationOfBinaryPropagationHandler() {
+    assertThat(traceComponentImplBase.getPropagationComponent())
+        .isInstanceOf(PropagationComponentImpl.class);
+  }
+
+  @Test
+  public void implementationOfClock() {
+    assertThat(traceComponentImplBase.getClock()).isInstanceOf(MillisClock.class);
+  }
+
+  @Test
+  public void implementationOfTraceExporter() {
+    assertThat(traceComponentImplBase.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/TracerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/TracerImplTest.java
new file mode 100644
index 0000000..d10be6a
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/TracerImplTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.implcore.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.config.TraceConfig;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link TracerImpl}. */
+@RunWith(JUnit4.class)
+public class TracerImplTest {
+  private static final String SPAN_NAME = "MySpanName";
+  @Mock private StartEndHandler startEndHandler;
+  @Mock private TraceConfig traceConfig;
+  private TracerImpl tracer;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    tracer =
+        new TracerImpl(new SecureRandomHandler(), startEndHandler, TestClock.create(), traceConfig);
+  }
+
+  @Test
+  public void createSpanBuilder() {
+    SpanBuilder spanBuilder = tracer.spanBuilderWithExplicitParent(SPAN_NAME, BlankSpan.INSTANCE);
+    assertThat(spanBuilder).isInstanceOf(SpanBuilderImpl.class);
+  }
+
+  @Test
+  public void createSpanBuilderWithRemoteParet() {
+    SpanBuilder spanBuilder = tracer.spanBuilderWithRemoteParent(SPAN_NAME, SpanContext.INVALID);
+    assertThat(spanBuilder).isInstanceOf(SpanBuilderImpl.class);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/config/TraceConfigImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/config/TraceConfigImplTest.java
new file mode 100644
index 0000000..ecaeda6
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/config/TraceConfigImplTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.implcore.trace.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.samplers.Samplers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceConfigImpl}. */
+@RunWith(JUnit4.class)
+public class TraceConfigImplTest {
+  private final TraceConfigImpl traceConfig = new TraceConfigImpl();
+
+  @Test
+  public void defaultActiveTraceParams() {
+    assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+  }
+
+  @Test
+  public void updateTraceParams() {
+    TraceParams traceParams =
+        TraceParams.DEFAULT
+            .toBuilder()
+            .setSampler(Samplers.alwaysSample())
+            .setMaxNumberOfAttributes(8)
+            .setMaxNumberOfAnnotations(9)
+            .setMaxNumberOfNetworkEvents(10)
+            .setMaxNumberOfLinks(11)
+            .build();
+    traceConfig.updateActiveTraceParams(traceParams);
+    assertThat(traceConfig.getActiveTraceParams()).isEqualTo(traceParams);
+    traceConfig.updateActiveTraceParams(TraceParams.DEFAULT);
+    assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/ExportComponentImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/ExportComponentImplTest.java
new file mode 100644
index 0000000..4b8993f
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/ExportComponentImplTest.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.implcore.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.trace.export.ExportComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ExportComponentImpl}. */
+@RunWith(JUnit4.class)
+public class ExportComponentImplTest {
+  private final ExportComponent exportComponentWithInProcess =
+      ExportComponentImpl.createWithInProcessStores(new SimpleEventQueue());
+  private final ExportComponent exportComponentWithoutInProcess =
+      ExportComponentImpl.createWithoutInProcessStores(new SimpleEventQueue());
+
+  @Test
+  public void implementationOfSpanExporter() {
+    assertThat(exportComponentWithInProcess.getSpanExporter()).isInstanceOf(SpanExporterImpl.class);
+  }
+
+  @Test
+  public void implementationOfActiveSpans() {
+    assertThat(exportComponentWithInProcess.getRunningSpanStore())
+        .isInstanceOf(InProcessRunningSpanStoreImpl.class);
+    assertThat(exportComponentWithoutInProcess.getRunningSpanStore())
+        .isInstanceOf(RunningSpanStoreImpl.getNoopRunningSpanStoreImpl().getClass());
+  }
+
+  @Test
+  public void implementationOfSampledSpanStore() {
+    assertThat(exportComponentWithInProcess.getSampledSpanStore())
+        .isInstanceOf(InProcessSampledSpanStoreImpl.class);
+    assertThat(exportComponentWithoutInProcess.getSampledSpanStore())
+        .isInstanceOf(SampledSpanStoreImpl.getNoopSampledSpanStoreImpl().getClass());
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImplTest.java
new file mode 100644
index 0000000..68ce1c1
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImplTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.implcore.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.StartEndHandlerImpl;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.RunningSpanStore.Filter;
+import java.util.Random;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InProcessRunningSpanStoreImpl}. */
+@RunWith(JUnit4.class)
+public class InProcessRunningSpanStoreImplTest {
+
+  private static final String SPAN_NAME_1 = "MySpanName/1";
+  private static final String SPAN_NAME_2 = "MySpanName/2";
+  private final Random random = new Random(1234);
+  private final SpanExporterImpl sampledSpansServiceExporter =
+      SpanExporterImpl.create(4, Duration.create(1, 0));
+  private final InProcessRunningSpanStoreImpl activeSpansExporter =
+      new InProcessRunningSpanStoreImpl();
+  private final StartEndHandler startEndHandler =
+      new StartEndHandlerImpl(
+          sampledSpansServiceExporter, activeSpansExporter, null, new SimpleEventQueue());
+
+  private RecordEventsSpanImpl createSpan(String spanName) {
+    final SpanContext spanContext =
+        SpanContext.create(
+            TraceId.generateRandomId(random),
+            SpanId.generateRandomId(random),
+            TraceOptions.DEFAULT);
+    return RecordEventsSpanImpl.startSpan(
+        spanContext,
+        spanName,
+        null,
+        SpanId.generateRandomId(random),
+        false,
+        TraceParams.DEFAULT,
+        startEndHandler,
+        null,
+        MillisClock.getInstance());
+  }
+
+  @Test
+  public void getSummary_SpansWithDifferentNames() {
+    final RecordEventsSpanImpl span1 = createSpan(SPAN_NAME_1);
+    final RecordEventsSpanImpl span2 = createSpan(SPAN_NAME_2);
+    assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(2);
+    assertThat(
+            activeSpansExporter
+                .getSummary()
+                .getPerSpanNameSummary()
+                .get(SPAN_NAME_1)
+                .getNumRunningSpans())
+        .isEqualTo(1);
+    assertThat(
+            activeSpansExporter
+                .getSummary()
+                .getPerSpanNameSummary()
+                .get(SPAN_NAME_2)
+                .getNumRunningSpans())
+        .isEqualTo(1);
+    span1.end();
+    assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(1);
+    assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().get(SPAN_NAME_1)).isNull();
+    assertThat(
+            activeSpansExporter
+                .getSummary()
+                .getPerSpanNameSummary()
+                .get(SPAN_NAME_2)
+                .getNumRunningSpans())
+        .isEqualTo(1);
+    span2.end();
+    assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(0);
+  }
+
+  @Test
+  public void getSummary_SpansWithSameName() {
+    final RecordEventsSpanImpl span1 = createSpan(SPAN_NAME_1);
+    final RecordEventsSpanImpl span2 = createSpan(SPAN_NAME_1);
+    final RecordEventsSpanImpl span3 = createSpan(SPAN_NAME_1);
+    assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(1);
+    assertThat(
+            activeSpansExporter
+                .getSummary()
+                .getPerSpanNameSummary()
+                .get(SPAN_NAME_1)
+                .getNumRunningSpans())
+        .isEqualTo(3);
+    span1.end();
+    assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(1);
+    assertThat(
+            activeSpansExporter
+                .getSummary()
+                .getPerSpanNameSummary()
+                .get(SPAN_NAME_1)
+                .getNumRunningSpans())
+        .isEqualTo(2);
+    span2.end();
+    assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(1);
+    assertThat(
+            activeSpansExporter
+                .getSummary()
+                .getPerSpanNameSummary()
+                .get(SPAN_NAME_1)
+                .getNumRunningSpans())
+        .isEqualTo(1);
+    span3.end();
+    assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(0);
+  }
+
+  @Test
+  public void getActiveSpans_SpansWithDifferentNames() {
+    RecordEventsSpanImpl span1 = createSpan(SPAN_NAME_1);
+    RecordEventsSpanImpl span2 = createSpan(SPAN_NAME_2);
+    assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 0)))
+        .containsExactly(span1.toSpanData());
+    assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 2)))
+        .containsExactly(span1.toSpanData());
+    assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_2, 0)))
+        .containsExactly(span2.toSpanData());
+    span1.end();
+    span2.end();
+  }
+
+  @Test
+  public void getActiveSpans_SpansWithSameName() {
+    RecordEventsSpanImpl span1 = createSpan(SPAN_NAME_1);
+    RecordEventsSpanImpl span2 = createSpan(SPAN_NAME_1);
+    RecordEventsSpanImpl span3 = createSpan(SPAN_NAME_1);
+    assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 0)))
+        .containsExactly(span1.toSpanData(), span2.toSpanData(), span3.toSpanData());
+    assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 2)).size())
+        .isEqualTo(2);
+    assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 2)))
+        .containsAnyOf(span1.toSpanData(), span2.toSpanData(), span3.toSpanData());
+    span1.end();
+    span2.end();
+    span3.end();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImplTest.java
new file mode 100644
index 0000000..7d8b434
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImplTest.java
@@ -0,0 +1,368 @@
+/*
+ * 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.implcore.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SampledSpanStore.ErrorFilter;
+import io.opencensus.trace.export.SampledSpanStore.LatencyBucketBoundaries;
+import io.opencensus.trace.export.SampledSpanStore.LatencyFilter;
+import io.opencensus.trace.export.SampledSpanStore.PerSpanNameSummary;
+import io.opencensus.trace.export.SpanData;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InProcessSampledSpanStoreImpl}. */
+@RunWith(JUnit4.class)
+public class InProcessSampledSpanStoreImplTest {
+  private static final String REGISTERED_SPAN_NAME = "MySpanName/1";
+  private static final String NOT_REGISTERED_SPAN_NAME = "MySpanName/2";
+  private static final long NUM_NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1);
+  private final Random random = new Random(1234);
+  private final SpanContext sampledSpanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random),
+          SpanId.generateRandomId(random),
+          TraceOptions.builder().setIsSampled(true).build());
+  private final SpanContext notSampledSpanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+  private final SpanId parentSpanId = SpanId.generateRandomId(random);
+  private final TestClock testClock = TestClock.create(Timestamp.create(12345, 54321));
+  private final InProcessSampledSpanStoreImpl sampleStore =
+      new InProcessSampledSpanStoreImpl(new SimpleEventQueue());
+  private final StartEndHandler startEndHandler =
+      new StartEndHandler() {
+        @Override
+        public void onStart(RecordEventsSpanImpl span) {
+          // Do nothing.
+        }
+
+        @Override
+        public void onEnd(RecordEventsSpanImpl span) {
+          sampleStore.considerForSampling(span);
+        }
+      };
+
+  @Before
+  public void setUp() {
+    sampleStore.registerSpanNamesForCollection(Collections.singletonList(REGISTERED_SPAN_NAME));
+  }
+
+  private RecordEventsSpanImpl createSampledSpan(String spanName) {
+    return RecordEventsSpanImpl.startSpan(
+        sampledSpanContext,
+        spanName,
+        null,
+        parentSpanId,
+        false,
+        TraceParams.DEFAULT,
+        startEndHandler,
+        null,
+        testClock);
+  }
+
+  private RecordEventsSpanImpl createNotSampledSpan(String spanName) {
+    return RecordEventsSpanImpl.startSpan(
+        notSampledSpanContext,
+        spanName,
+        null,
+        parentSpanId,
+        false,
+        TraceParams.DEFAULT,
+        startEndHandler,
+        null,
+        testClock);
+  }
+
+  private void addSpanNameToAllLatencyBuckets(String spanName) {
+    for (LatencyBucketBoundaries boundaries : LatencyBucketBoundaries.values()) {
+      Span sampledSpan = createSampledSpan(spanName);
+      Span notSampledSpan = createNotSampledSpan(spanName);
+      if (boundaries.getLatencyLowerNs() < NUM_NANOS_PER_SECOND) {
+        testClock.advanceTime(Duration.create(0, (int) boundaries.getLatencyLowerNs()));
+      } else {
+        testClock.advanceTime(
+            Duration.create(
+                boundaries.getLatencyLowerNs() / NUM_NANOS_PER_SECOND,
+                (int) (boundaries.getLatencyLowerNs() % NUM_NANOS_PER_SECOND)));
+      }
+      sampledSpan.end();
+      notSampledSpan.end();
+    }
+  }
+
+  private void addSpanNameToAllErrorBuckets(String spanName) {
+    for (CanonicalCode code : CanonicalCode.values()) {
+      if (code != CanonicalCode.OK) {
+        Span sampledSpan = createSampledSpan(spanName);
+        Span notSampledSpan = createNotSampledSpan(spanName);
+        testClock.advanceTime(Duration.create(0, 1000));
+        sampledSpan.end(EndSpanOptions.builder().setStatus(code.toStatus()).build());
+        notSampledSpan.end(EndSpanOptions.builder().setStatus(code.toStatus()).build());
+      }
+    }
+  }
+
+  @Test
+  public void addSpansWithRegisteredNamesInAllLatencyBuckets() {
+    addSpanNameToAllLatencyBuckets(REGISTERED_SPAN_NAME);
+    Map<String, PerSpanNameSummary> perSpanNameSummary =
+        sampleStore.getSummary().getPerSpanNameSummary();
+    assertThat(perSpanNameSummary.size()).isEqualTo(1);
+    Map<LatencyBucketBoundaries, Integer> latencyBucketsSummaries =
+        perSpanNameSummary.get(REGISTERED_SPAN_NAME).getNumbersOfLatencySampledSpans();
+    assertThat(latencyBucketsSummaries.size()).isEqualTo(LatencyBucketBoundaries.values().length);
+    for (Map.Entry<LatencyBucketBoundaries, Integer> it : latencyBucketsSummaries.entrySet()) {
+      assertThat(it.getValue()).isEqualTo(2);
+    }
+  }
+
+  @Test
+  public void addSpansWithoutRegisteredNamesInAllLatencyBuckets() {
+    addSpanNameToAllLatencyBuckets(NOT_REGISTERED_SPAN_NAME);
+    Map<String, PerSpanNameSummary> perSpanNameSummary =
+        sampleStore.getSummary().getPerSpanNameSummary();
+    assertThat(perSpanNameSummary.size()).isEqualTo(1);
+    assertThat(perSpanNameSummary.containsKey(NOT_REGISTERED_SPAN_NAME)).isFalse();
+  }
+
+  @Test
+  public void registerUnregisterAndListSpanNames() {
+    assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+        .containsExactly(REGISTERED_SPAN_NAME);
+    sampleStore.registerSpanNamesForCollection(Collections.singletonList(NOT_REGISTERED_SPAN_NAME));
+    assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+        .containsExactly(REGISTERED_SPAN_NAME, NOT_REGISTERED_SPAN_NAME);
+    sampleStore.unregisterSpanNamesForCollection(
+        Collections.singletonList(NOT_REGISTERED_SPAN_NAME));
+    assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+        .containsExactly(REGISTERED_SPAN_NAME);
+  }
+
+  @Test
+  public void registerSpanNamesViaSpanBuilderOption() {
+    assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+        .containsExactly(REGISTERED_SPAN_NAME);
+    createSampledSpan(NOT_REGISTERED_SPAN_NAME)
+        .end(EndSpanOptions.builder().setSampleToLocalSpanStore(true).build());
+    assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+        .containsExactly(REGISTERED_SPAN_NAME, NOT_REGISTERED_SPAN_NAME);
+  }
+
+  @Test
+  public void addSpansWithRegisteredNamesInAllErrorBuckets() {
+    addSpanNameToAllErrorBuckets(REGISTERED_SPAN_NAME);
+    Map<String, PerSpanNameSummary> perSpanNameSummary =
+        sampleStore.getSummary().getPerSpanNameSummary();
+    assertThat(perSpanNameSummary.size()).isEqualTo(1);
+    Map<CanonicalCode, Integer> errorBucketsSummaries =
+        perSpanNameSummary.get(REGISTERED_SPAN_NAME).getNumbersOfErrorSampledSpans();
+    assertThat(errorBucketsSummaries.size()).isEqualTo(CanonicalCode.values().length - 1);
+    for (Map.Entry<CanonicalCode, Integer> it : errorBucketsSummaries.entrySet()) {
+      assertThat(it.getValue()).isEqualTo(2);
+    }
+  }
+
+  @Test
+  public void addSpansWithoutRegisteredNamesInAllErrorBuckets() {
+    addSpanNameToAllErrorBuckets(NOT_REGISTERED_SPAN_NAME);
+    Map<String, PerSpanNameSummary> perSpanNameSummary =
+        sampleStore.getSummary().getPerSpanNameSummary();
+    assertThat(perSpanNameSummary.size()).isEqualTo(1);
+    assertThat(perSpanNameSummary.containsKey(NOT_REGISTERED_SPAN_NAME)).isFalse();
+  }
+
+  @Test
+  public void getErrorSampledSpans() {
+    RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, 1000));
+    span.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+    Collection<SpanData> samples =
+        sampleStore.getErrorSampledSpans(
+            ErrorFilter.create(REGISTERED_SPAN_NAME, CanonicalCode.CANCELLED, 0));
+    assertThat(samples.size()).isEqualTo(1);
+    assertThat(samples.contains(span.toSpanData())).isTrue();
+  }
+
+  @Test
+  public void getErrorSampledSpans_MaxSpansToReturn() {
+    RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, 1000));
+    span1.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+    // Advance time to allow other spans to be sampled.
+    testClock.advanceTime(Duration.create(5, 0));
+    RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, 1000));
+    span2.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+    Collection<SpanData> samples =
+        sampleStore.getErrorSampledSpans(
+            ErrorFilter.create(REGISTERED_SPAN_NAME, CanonicalCode.CANCELLED, 1));
+    assertThat(samples.size()).isEqualTo(1);
+    // No order guaranteed so one of the spans should be in the list.
+    assertThat(samples).containsAnyOf(span1.toSpanData(), span2.toSpanData());
+  }
+
+  @Test
+  public void getErrorSampledSpans_NullCode() {
+    RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, 1000));
+    span1.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+    RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, 1000));
+    span2.end(EndSpanOptions.builder().setStatus(Status.UNKNOWN).build());
+    Collection<SpanData> samples =
+        sampleStore.getErrorSampledSpans(ErrorFilter.create(REGISTERED_SPAN_NAME, null, 0));
+    assertThat(samples.size()).isEqualTo(2);
+    assertThat(samples).containsExactly(span1.toSpanData(), span2.toSpanData());
+  }
+
+  @Test
+  public void getErrorSampledSpans_NullCode_MaxSpansToReturn() {
+    RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, 1000));
+    span1.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+    RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, 1000));
+    span2.end(EndSpanOptions.builder().setStatus(Status.UNKNOWN).build());
+    Collection<SpanData> samples =
+        sampleStore.getErrorSampledSpans(ErrorFilter.create(REGISTERED_SPAN_NAME, null, 1));
+    assertThat(samples.size()).isEqualTo(1);
+    assertThat(samples).containsAnyOf(span1.toSpanData(), span2.toSpanData());
+  }
+
+  @Test
+  public void getLatencySampledSpans() {
+    RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+    span.end();
+    Collection<SpanData> samples =
+        sampleStore.getLatencySampledSpans(
+            LatencyFilter.create(
+                REGISTERED_SPAN_NAME,
+                TimeUnit.MICROSECONDS.toNanos(15),
+                TimeUnit.MICROSECONDS.toNanos(25),
+                0));
+    assertThat(samples.size()).isEqualTo(1);
+    assertThat(samples.contains(span.toSpanData())).isTrue();
+  }
+
+  @Test
+  public void getLatencySampledSpans_ExclusiveUpperBound() {
+    RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+    span.end();
+    Collection<SpanData> samples =
+        sampleStore.getLatencySampledSpans(
+            LatencyFilter.create(
+                REGISTERED_SPAN_NAME,
+                TimeUnit.MICROSECONDS.toNanos(15),
+                TimeUnit.MICROSECONDS.toNanos(20),
+                0));
+    assertThat(samples.size()).isEqualTo(0);
+  }
+
+  @Test
+  public void getLatencySampledSpans_InclusiveLowerBound() {
+    RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+    span.end();
+    Collection<SpanData> samples =
+        sampleStore.getLatencySampledSpans(
+            LatencyFilter.create(
+                REGISTERED_SPAN_NAME,
+                TimeUnit.MICROSECONDS.toNanos(20),
+                TimeUnit.MICROSECONDS.toNanos(25),
+                0));
+    assertThat(samples.size()).isEqualTo(1);
+    assertThat(samples.contains(span.toSpanData())).isTrue();
+  }
+
+  @Test
+  public void getLatencySampledSpans_QueryBetweenMultipleBuckets() {
+    RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+    span1.end();
+    // Advance time to allow other spans to be sampled.
+    testClock.advanceTime(Duration.create(5, 0));
+    RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(200)));
+    span2.end();
+    Collection<SpanData> samples =
+        sampleStore.getLatencySampledSpans(
+            LatencyFilter.create(
+                REGISTERED_SPAN_NAME,
+                TimeUnit.MICROSECONDS.toNanos(15),
+                TimeUnit.MICROSECONDS.toNanos(250),
+                0));
+    assertThat(samples).containsExactly(span1.toSpanData(), span2.toSpanData());
+  }
+
+  @Test
+  public void getLatencySampledSpans_MaxSpansToReturn() {
+    RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+    span1.end();
+    // Advance time to allow other spans to be sampled.
+    testClock.advanceTime(Duration.create(5, 0));
+    RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(200)));
+    span2.end();
+    Collection<SpanData> samples =
+        sampleStore.getLatencySampledSpans(
+            LatencyFilter.create(
+                REGISTERED_SPAN_NAME,
+                TimeUnit.MICROSECONDS.toNanos(15),
+                TimeUnit.MICROSECONDS.toNanos(250),
+                1));
+    assertThat(samples.size()).isEqualTo(1);
+    assertThat(samples.contains(span1.toSpanData())).isTrue();
+  }
+
+  @Test
+  public void ignoreNegativeSpanLatency() {
+    RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+    testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(-20)));
+    span.end();
+    Collection<SpanData> samples =
+        sampleStore.getLatencySampledSpans(
+            LatencyFilter.create(REGISTERED_SPAN_NAME, 0, Long.MAX_VALUE, 0));
+    assertThat(samples.size()).isEqualTo(0);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopRunningSpanStoreImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopRunningSpanStoreImplTest.java
new file mode 100644
index 0000000..96669df
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopRunningSpanStoreImplTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2018, 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.implcore.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.RunningSpanStore.Filter;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link RunningSpanStoreImpl.NoopRunningSpanStoreImpl}. */
+@RunWith(JUnit4.class)
+public class NoopRunningSpanStoreImplTest {
+
+  private static final String SPAN_NAME = "MySpanName";
+
+  private final Timestamp timestamp = Timestamp.create(1234, 5678);
+  private final Random random = new Random(1234);
+  private final SpanContext spanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+  private final TestClock testClock = TestClock.create(timestamp);
+  private final TimestampConverter timestampConverter = TimestampConverter.now(testClock);
+  @Mock private StartEndHandler startEndHandler;
+  private RecordEventsSpanImpl recordEventsSpanImpl;
+  // maxSpansToReturn=0 means all
+  private final Filter filter = Filter.create(SPAN_NAME, 0 /* maxSpansToReturn */);
+  private final EventQueue eventQueue = new SimpleEventQueue();
+  private final RunningSpanStoreImpl runningSpanStoreImpl =
+      ExportComponentImpl.createWithoutInProcessStores(eventQueue).getRunningSpanStore();
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    recordEventsSpanImpl =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            null,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+  }
+
+  private void getMethodsShouldReturnEmpty() {
+    // get methods should always return empty collections.
+    assertThat(runningSpanStoreImpl.getSummary().getPerSpanNameSummary()).isEmpty();
+    assertThat(runningSpanStoreImpl.getRunningSpans(filter)).isEmpty();
+  }
+
+  @Test
+  public void noopImplementation() {
+    getMethodsShouldReturnEmpty();
+    // onStart() does not affect the result.
+    runningSpanStoreImpl.onStart(recordEventsSpanImpl);
+    getMethodsShouldReturnEmpty();
+    // onEnd() does not affect the result.
+    runningSpanStoreImpl.onEnd(recordEventsSpanImpl);
+    getMethodsShouldReturnEmpty();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopSampledSpanStoreImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopSampledSpanStoreImplTest.java
new file mode 100644
index 0000000..b9fbd43
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopSampledSpanStoreImplTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018, 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.implcore.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SampledSpanStore.ErrorFilter;
+import io.opencensus.trace.export.SampledSpanStore.LatencyFilter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link SampledSpanStoreImpl.NoopSampledSpanStoreImpl}. */
+@RunWith(JUnit4.class)
+public final class NoopSampledSpanStoreImplTest {
+
+  private static final String SPAN_NAME = "MySpanName";
+  private static final Collection<String> NAMES_FOR_COLLECTION =
+      Collections.<String>singletonList(SPAN_NAME);
+
+  private final Timestamp timestamp = Timestamp.create(1234, 5678);
+  private final Random random = new Random(1234);
+  private final SpanContext spanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+  private final TestClock testClock = TestClock.create(timestamp);
+  private final TimestampConverter timestampConverter = TimestampConverter.now(testClock);
+  @Mock private StartEndHandler startEndHandler;
+  private RecordEventsSpanImpl recordEventsSpanImpl;
+  // maxSpansToReturn=0 means all
+  private final ErrorFilter errorFilter =
+      ErrorFilter.create(SPAN_NAME, null /* canonicalCode */, 0 /* maxSpansToReturn */);
+  private final LatencyFilter latencyFilter =
+      LatencyFilter.create(
+          SPAN_NAME,
+          0 /* latencyLowerNs */,
+          Long.MAX_VALUE /* latencyUpperNs */,
+          0 /* maxSpansToReturn */);
+  private final EventQueue eventQueue = new SimpleEventQueue();
+  private final SampledSpanStoreImpl sampledSpanStoreImpl =
+      ExportComponentImpl.createWithoutInProcessStores(eventQueue).getSampledSpanStore();
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  private void getMethodsShouldReturnEmpty() {
+    // get methods always return empty collections.
+    assertThat(sampledSpanStoreImpl.getSummary().getPerSpanNameSummary()).isEmpty();
+    assertThat(sampledSpanStoreImpl.getRegisteredSpanNamesForCollection()).isEmpty();
+    assertThat(sampledSpanStoreImpl.getErrorSampledSpans(errorFilter)).isEmpty();
+    assertThat(sampledSpanStoreImpl.getLatencySampledSpans(latencyFilter)).isEmpty();
+  }
+
+  @Test
+  public void noopImplementation() {
+    // None of the get methods should yield non-empty result.
+    getMethodsShouldReturnEmpty();
+
+    // registerSpanNamesForCollection() should do nothing and do not affect the result.
+    sampledSpanStoreImpl.registerSpanNamesForCollection(NAMES_FOR_COLLECTION);
+    getMethodsShouldReturnEmpty();
+
+    // considerForSampling() should do nothing and do not affect the result.
+    // It should be called after registerSpanNamesForCollection.
+    recordEventsSpanImpl =
+        RecordEventsSpanImpl.startSpan(
+            spanContext,
+            SPAN_NAME,
+            null,
+            null,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            timestampConverter,
+            testClock);
+    recordEventsSpanImpl.end();
+    sampledSpanStoreImpl.considerForSampling(recordEventsSpanImpl);
+    getMethodsShouldReturnEmpty();
+
+    // unregisterSpanNamesForCollection() should do nothing and do not affect the result.
+    sampledSpanStoreImpl.unregisterSpanNamesForCollection(NAMES_FOR_COLLECTION);
+    getMethodsShouldReturnEmpty();
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/SpanExporterImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/SpanExporterImplTest.java
new file mode 100644
index 0000000..f8f1d91
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/SpanExporterImplTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.implcore.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.anyListOf;
+import static org.mockito.Mockito.doThrow;
+
+import io.opencensus.common.Duration;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.StartEndHandlerImpl;
+import io.opencensus.testing.export.TestHandler;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.util.List;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link SpanExporterImpl}. */
+@RunWith(JUnit4.class)
+public class SpanExporterImplTest {
+  private static final String SPAN_NAME_1 = "MySpanName/1";
+  private static final String SPAN_NAME_2 = "MySpanName/2";
+  private final Random random = new Random(1234);
+  private final SpanContext sampledSpanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random),
+          SpanId.generateRandomId(random),
+          TraceOptions.builder().setIsSampled(true).build());
+  private final SpanContext notSampledSpanContext =
+      SpanContext.create(
+          TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+  private final RunningSpanStoreImpl runningSpanStore = new InProcessRunningSpanStoreImpl();
+  private final TestHandler serviceHandler = new TestHandler();
+  @Mock private Handler mockServiceHandler;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  private RecordEventsSpanImpl createSampledEndedSpan(
+      StartEndHandler startEndHandler, String spanName) {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            sampledSpanContext,
+            spanName,
+            null,
+            null,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            null,
+            MillisClock.getInstance());
+    span.end();
+    return span;
+  }
+
+  private RecordEventsSpanImpl createNotSampledEndedSpan(
+      StartEndHandler startEndHandler, String spanName) {
+    RecordEventsSpanImpl span =
+        RecordEventsSpanImpl.startSpan(
+            notSampledSpanContext,
+            spanName,
+            null,
+            null,
+            false,
+            TraceParams.DEFAULT,
+            startEndHandler,
+            null,
+            MillisClock.getInstance());
+    span.end();
+    return span;
+  }
+
+  @Test
+  public void exportDifferentSampledSpans() {
+    SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+    StartEndHandler startEndHandler =
+        new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+    spanExporter.registerHandler("test.service", serviceHandler);
+
+    RecordEventsSpanImpl span1 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_2);
+    List<SpanData> exported = serviceHandler.waitForExport(2);
+    assertThat(exported).containsExactly(span1.toSpanData(), span2.toSpanData());
+  }
+
+  @Test
+  public void exportMoreSpansThanTheBufferSize() {
+    SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+    StartEndHandler startEndHandler =
+        new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+    spanExporter.registerHandler("test.service", serviceHandler);
+
+    RecordEventsSpanImpl span1 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    RecordEventsSpanImpl span3 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    RecordEventsSpanImpl span4 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    RecordEventsSpanImpl span5 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    RecordEventsSpanImpl span6 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    List<SpanData> exported = serviceHandler.waitForExport(6);
+    assertThat(exported)
+        .containsExactly(
+            span1.toSpanData(),
+            span2.toSpanData(),
+            span3.toSpanData(),
+            span4.toSpanData(),
+            span5.toSpanData(),
+            span6.toSpanData());
+  }
+
+  @Test
+  public void interruptWorkerThreadStops() throws InterruptedException {
+    SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+
+    spanExporter.registerHandler("test.service", serviceHandler);
+
+    Thread serviceExporterThread = spanExporter.getServiceExporterThread();
+    serviceExporterThread.interrupt();
+    // Test that the worker thread will stop.
+    serviceExporterThread.join();
+  }
+
+  @Test
+  public void serviceHandlerThrowsException() {
+    doThrow(new IllegalArgumentException("No export for you."))
+        .when(mockServiceHandler)
+        .export(anyListOf(SpanData.class));
+
+    SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+    StartEndHandler startEndHandler =
+        new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+    spanExporter.registerHandler("test.service", serviceHandler);
+
+    spanExporter.registerHandler("mock.service", mockServiceHandler);
+    RecordEventsSpanImpl span1 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    List<SpanData> exported = serviceHandler.waitForExport(1);
+    assertThat(exported).containsExactly(span1.toSpanData());
+    // Continue to export after the exception was received.
+    RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    exported = serviceHandler.waitForExport(1);
+    assertThat(exported).containsExactly(span2.toSpanData());
+  }
+
+  @Test
+  public void exportSpansToMultipleServices() {
+    SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+    StartEndHandler startEndHandler =
+        new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+    spanExporter.registerHandler("test.service", serviceHandler);
+
+    TestHandler serviceHandler2 = new TestHandler();
+    spanExporter.registerHandler("test.service2", serviceHandler2);
+    RecordEventsSpanImpl span1 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_2);
+    List<SpanData> exported1 = serviceHandler.waitForExport(2);
+    List<SpanData> exported2 = serviceHandler2.waitForExport(2);
+    assertThat(exported1).containsExactly(span1.toSpanData(), span2.toSpanData());
+    assertThat(exported2).containsExactly(span1.toSpanData(), span2.toSpanData());
+  }
+
+  @Test
+  public void exportNotSampledSpans() {
+    SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+    StartEndHandler startEndHandler =
+        new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+    spanExporter.registerHandler("test.service", serviceHandler);
+
+    RecordEventsSpanImpl span1 = createNotSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+    RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_2);
+    // Spans are recorded and exported in the same order as they are ended, we test that a non
+    // sampled span is not exported by creating and ending a sampled span after a non sampled span
+    // and checking that the first exported span is the sampled span (the non sampled did not get
+    // exported).
+    List<SpanData> exported = serviceHandler.waitForExport(1);
+    // Need to check this because otherwise the variable span1 is unused, other option is to not
+    // have a span1 variable.
+    assertThat(exported).doesNotContain(span1.toSpanData());
+    assertThat(exported).containsExactly(span2.toSpanData());
+  }
+
+  @Test(timeout = 10000L)
+  public void exportNotSampledSpansFlushed() {
+    // Set the export delay to zero, for no timeout, in order to confirm the #flush() below works
+    SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(0, 0));
+    StartEndHandler startEndHandler =
+        new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+    spanExporter.registerHandler("test.service", serviceHandler);
+
+    RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_2);
+
+    // Force a flush, without this, the #waitForExport() call below would block indefinitely.
+    spanExporter.flush();
+
+    List<SpanData> exported = serviceHandler.waitForExport(1);
+
+    assertThat(exported).containsExactly(span2.toSpanData());
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveListTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveListTest.java
new file mode 100644
index 0000000..d7ac2ae
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveListTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.implcore.trace.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.trace.internal.ConcurrentIntrusiveList.Element;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ConcurrentIntrusiveList}. */
+@RunWith(JUnit4.class)
+public class ConcurrentIntrusiveListTest {
+  private final ConcurrentIntrusiveList<FakeElement> intrusiveList =
+      new ConcurrentIntrusiveList<FakeElement>();
+  @Rule public final ExpectedException exception = ExpectedException.none();
+
+  @Test
+  public void emptyList() {
+    assertThat(intrusiveList.size()).isEqualTo(0);
+    assertThat(intrusiveList.getAll().isEmpty()).isTrue();
+  }
+
+  @Test
+  public void addRemoveAdd_SameElement() {
+    FakeElement element = new FakeElement();
+    intrusiveList.addElement(element);
+    assertThat(intrusiveList.size()).isEqualTo(1);
+    intrusiveList.removeElement(element);
+    assertThat(intrusiveList.size()).isEqualTo(0);
+    intrusiveList.addElement(element);
+    assertThat(intrusiveList.size()).isEqualTo(1);
+  }
+
+  @Test
+  public void addAndRemoveElements() {
+    FakeElement element1 = new FakeElement();
+    FakeElement element2 = new FakeElement();
+    FakeElement element3 = new FakeElement();
+    intrusiveList.addElement(element1);
+    intrusiveList.addElement(element2);
+    intrusiveList.addElement(element3);
+    assertThat(intrusiveList.size()).isEqualTo(3);
+    assertThat(intrusiveList.getAll()).containsExactly(element3, element2, element1).inOrder();
+    // Remove element from the middle of the list.
+    intrusiveList.removeElement(element2);
+    assertThat(intrusiveList.size()).isEqualTo(2);
+    assertThat(intrusiveList.getAll()).containsExactly(element3, element1).inOrder();
+    // Remove element from the tail of the list.
+    intrusiveList.removeElement(element1);
+    assertThat(intrusiveList.size()).isEqualTo(1);
+    assertThat(intrusiveList.getAll().contains(element3)).isTrue();
+    intrusiveList.addElement(element1);
+    assertThat(intrusiveList.size()).isEqualTo(2);
+    assertThat(intrusiveList.getAll()).containsExactly(element1, element3).inOrder();
+    // Remove element from the head of the list when there are other elements after.
+    intrusiveList.removeElement(element1);
+    assertThat(intrusiveList.size()).isEqualTo(1);
+    assertThat(intrusiveList.getAll().contains(element3)).isTrue();
+    // Remove element from the head of the list when no more other elements in the list.
+    intrusiveList.removeElement(element3);
+    assertThat(intrusiveList.size()).isEqualTo(0);
+    assertThat(intrusiveList.getAll().isEmpty()).isTrue();
+  }
+
+  @Test
+  public void addAlreadyAddedElement() {
+    FakeElement element = new FakeElement();
+    intrusiveList.addElement(element);
+    exception.expect(IllegalArgumentException.class);
+    intrusiveList.addElement(element);
+  }
+
+  @Test
+  public void removeNotAddedElement() {
+    FakeElement element = new FakeElement();
+    exception.expect(IllegalArgumentException.class);
+    intrusiveList.removeElement(element);
+  }
+
+  private static final class FakeElement implements Element<FakeElement> {
+    @Nullable private FakeElement next = null;
+    @Nullable private FakeElement prev = null;
+
+    @Override
+    public FakeElement getNext() {
+      return next;
+    }
+
+    @Override
+    public void setNext(FakeElement element) {
+      next = element;
+    }
+
+    @Override
+    public FakeElement getPrev() {
+      return prev;
+    }
+
+    @Override
+    public void setPrev(FakeElement element) {
+      prev = element;
+    }
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/B3FormatTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/B3FormatTest.java
new file mode 100644
index 0000000..52e6bb3
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/B3FormatTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.implcore.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_FLAGS;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_PARENT_SPAN_ID;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_SAMPLED;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_SPAN_ID;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_TRACE_ID;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link B3Format}. */
+@RunWith(JUnit4.class)
+public class B3FormatTest {
+  private static final String TRACE_ID_BASE16 = "ff000000000000000000000000000041";
+  private static final TraceId TRACE_ID = TraceId.fromLowerBase16(TRACE_ID_BASE16);
+  private static final String TRACE_ID_BASE16_EIGHT_BYTES = "0000000000000041";
+  private static final TraceId TRACE_ID_EIGHT_BYTES =
+      TraceId.fromLowerBase16("0000000000000000" + TRACE_ID_BASE16_EIGHT_BYTES);
+  private static final String SPAN_ID_BASE16 = "ff00000000000041";
+  private static final SpanId SPAN_ID = SpanId.fromLowerBase16(SPAN_ID_BASE16);
+  private static final byte TRACE_OPTIONS_BYTE = 1;
+  private static final TraceOptions TRACE_OPTIONS = TraceOptions.fromByte(TRACE_OPTIONS_BYTE);
+  private static final Setter<Map<String, String>> setter =
+      new Setter<Map<String, String>>() {
+        @Override
+        public void put(Map<String, String> carrier, String key, String value) {
+          carrier.put(key, value);
+        }
+      };
+  private static final Getter<Map<String, String>> getter =
+      new Getter<Map<String, String>>() {
+        @Nullable
+        @Override
+        public String get(Map<String, String> carrier, String key) {
+          return carrier.get(key);
+        }
+      };
+  private final B3Format b3Format = new B3Format();
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void serialize_SampledContext() {
+    Map<String, String> carrier = new HashMap<String, String>();
+    b3Format.inject(SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS), carrier, setter);
+    assertThat(carrier)
+        .containsExactly(
+            X_B3_TRACE_ID, TRACE_ID_BASE16, X_B3_SPAN_ID, SPAN_ID_BASE16, X_B3_SAMPLED, "1");
+  }
+
+  @Test
+  public void serialize_NotSampledContext() {
+    Map<String, String> carrier = new HashMap<String, String>();
+    b3Format.inject(SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT), carrier, setter);
+    assertThat(carrier)
+        .containsExactly(X_B3_TRACE_ID, TRACE_ID_BASE16, X_B3_SPAN_ID, SPAN_ID_BASE16);
+  }
+
+  @Test
+  public void parseMissingSampledAndMissingFlag() throws SpanContextParseException {
+    Map<String, String> headersNotSampled = new HashMap<String, String>();
+    headersNotSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+    headersNotSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    SpanContext spanContext = SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT);
+    assertThat(b3Format.extract(headersNotSampled, getter)).isEqualTo(spanContext);
+  }
+
+  @Test
+  public void parseSampled() throws SpanContextParseException {
+    Map<String, String> headersSampled = new HashMap<String, String>();
+    headersSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+    headersSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    headersSampled.put(X_B3_SAMPLED, "1");
+    assertThat(b3Format.extract(headersSampled, getter))
+        .isEqualTo(SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS));
+  }
+
+  @Test
+  public void parseZeroSampled() throws SpanContextParseException {
+    Map<String, String> headersNotSampled = new HashMap<String, String>();
+    headersNotSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+    headersNotSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    headersNotSampled.put(X_B3_SAMPLED, "0");
+    assertThat(b3Format.extract(headersNotSampled, getter))
+        .isEqualTo(SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT));
+  }
+
+  @Test
+  public void parseFlag() throws SpanContextParseException {
+    Map<String, String> headersFlagSampled = new HashMap<String, String>();
+    headersFlagSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+    headersFlagSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    headersFlagSampled.put(X_B3_FLAGS, "1");
+    assertThat(b3Format.extract(headersFlagSampled, getter))
+        .isEqualTo(SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS));
+  }
+
+  @Test
+  public void parseZeroFlag() throws SpanContextParseException {
+    Map<String, String> headersFlagNotSampled = new HashMap<String, String>();
+    headersFlagNotSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+    headersFlagNotSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    headersFlagNotSampled.put(X_B3_FLAGS, "0");
+    assertThat(b3Format.extract(headersFlagNotSampled, getter))
+        .isEqualTo(SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT));
+  }
+
+  @Test
+  public void parseEightBytesTraceId() throws SpanContextParseException {
+    Map<String, String> headersEightBytes = new HashMap<String, String>();
+    headersEightBytes.put(X_B3_TRACE_ID, TRACE_ID_BASE16_EIGHT_BYTES);
+    headersEightBytes.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    headersEightBytes.put(X_B3_SAMPLED, "1");
+    assertThat(b3Format.extract(headersEightBytes, getter))
+        .isEqualTo(SpanContext.create(TRACE_ID_EIGHT_BYTES, SPAN_ID, TRACE_OPTIONS));
+  }
+
+  @Test
+  public void parseEightBytesTraceId_NotSampledSpanContext() throws SpanContextParseException {
+    Map<String, String> headersEightBytes = new HashMap<String, String>();
+    headersEightBytes.put(X_B3_TRACE_ID, TRACE_ID_BASE16_EIGHT_BYTES);
+    headersEightBytes.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    assertThat(b3Format.extract(headersEightBytes, getter))
+        .isEqualTo(SpanContext.create(TRACE_ID_EIGHT_BYTES, SPAN_ID, TraceOptions.DEFAULT));
+  }
+
+  @Test
+  public void parseInvalidTraceId() throws SpanContextParseException {
+    Map<String, String> invalidHeaders = new HashMap<String, String>();
+    invalidHeaders.put(X_B3_TRACE_ID, "abcdefghijklmnop");
+    invalidHeaders.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    thrown.expect(SpanContextParseException.class);
+    thrown.expectMessage("Invalid input.");
+    b3Format.extract(invalidHeaders, getter);
+  }
+
+  @Test
+  public void parseInvalidTraceId_Size() throws SpanContextParseException {
+    Map<String, String> invalidHeaders = new HashMap<String, String>();
+    invalidHeaders.put(X_B3_TRACE_ID, "0123456789abcdef00");
+    invalidHeaders.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    thrown.expect(SpanContextParseException.class);
+    thrown.expectMessage("Invalid input.");
+    b3Format.extract(invalidHeaders, getter);
+  }
+
+  @Test
+  public void parseMissingTraceId() throws SpanContextParseException {
+    Map<String, String> invalidHeaders = new HashMap<String, String>();
+    invalidHeaders.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+    thrown.expect(SpanContextParseException.class);
+    thrown.expectMessage("Missing X_B3_TRACE_ID.");
+    b3Format.extract(invalidHeaders, getter);
+  }
+
+  @Test
+  public void parseInvalidSpanId() throws SpanContextParseException {
+    Map<String, String> invalidHeaders = new HashMap<String, String>();
+    invalidHeaders.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+    invalidHeaders.put(X_B3_SPAN_ID, "abcdefghijklmnop");
+    thrown.expect(SpanContextParseException.class);
+    thrown.expectMessage("Invalid input.");
+    b3Format.extract(invalidHeaders, getter);
+  }
+
+  @Test
+  public void parseInvalidSpanId_Size() throws SpanContextParseException {
+    Map<String, String> invalidHeaders = new HashMap<String, String>();
+    invalidHeaders.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+    invalidHeaders.put(X_B3_SPAN_ID, "0123456789abcdef00");
+    thrown.expect(SpanContextParseException.class);
+    thrown.expectMessage("Invalid input.");
+    b3Format.extract(invalidHeaders, getter);
+  }
+
+  @Test
+  public void parseMissingSpanId() throws SpanContextParseException {
+    Map<String, String> invalidHeaders = new HashMap<String, String>();
+    invalidHeaders.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+    thrown.expect(SpanContextParseException.class);
+    thrown.expectMessage("Missing X_B3_SPAN_ID.");
+    b3Format.extract(invalidHeaders, getter);
+  }
+
+  @Test
+  public void fields_list() {
+    assertThat(b3Format.fields())
+        .containsExactly(
+            X_B3_TRACE_ID, X_B3_SPAN_ID, X_B3_PARENT_SPAN_ID, X_B3_SAMPLED, X_B3_FLAGS);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplTest.java
new file mode 100644
index 0000000..f43be47
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.implcore.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.propagation.BinaryFormat;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BinaryFormatImpl}. */
+@RunWith(JUnit4.class)
+public class BinaryFormatImplTest {
+  private static final byte[] TRACE_ID_BYTES =
+      new byte[] {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
+  private static final TraceId TRACE_ID = TraceId.fromBytes(TRACE_ID_BYTES);
+  private static final byte[] SPAN_ID_BYTES = new byte[] {97, 98, 99, 100, 101, 102, 103, 104};
+  private static final SpanId SPAN_ID = SpanId.fromBytes(SPAN_ID_BYTES);
+  private static final byte TRACE_OPTIONS_BYTES = 1;
+  private static final TraceOptions TRACE_OPTIONS = TraceOptions.fromByte(TRACE_OPTIONS_BYTES);
+  private static final byte[] EXAMPLE_BYTES =
+      new byte[] {
+        0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99, 100,
+        101, 102, 103, 104, 2, 1
+      };
+  private static final SpanContext EXAMPLE_SPAN_CONTEXT =
+      SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS);
+  @Rule public ExpectedException expectedException = ExpectedException.none();
+  private final BinaryFormat binaryFormat = new BinaryFormatImpl();
+
+  private void testSpanContextConversion(SpanContext spanContext) throws SpanContextParseException {
+    SpanContext propagatedBinarySpanContext =
+        binaryFormat.fromByteArray(binaryFormat.toByteArray(spanContext));
+
+    assertWithMessage("BinaryFormat propagated context is not equal with the initial context.")
+        .that(propagatedBinarySpanContext)
+        .isEqualTo(spanContext);
+  }
+
+  @Test
+  public void propagate_SpanContextTracingEnabled() throws SpanContextParseException {
+    testSpanContextConversion(
+        SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.builder().setIsSampled(true).build()));
+  }
+
+  @Test
+  public void propagate_SpanContextNoTracing() throws SpanContextParseException {
+    testSpanContextConversion(SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT));
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void toBinaryValue_NullSpanContext() {
+    binaryFormat.toByteArray(null);
+  }
+
+  @Test
+  public void toBinaryValue_InvalidSpanContext() {
+    assertThat(binaryFormat.toByteArray(SpanContext.INVALID))
+        .isEqualTo(
+            new byte[] {
+              0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0
+            });
+  }
+
+  @Test
+  public void fromBinaryValue_BinaryExampleValue() throws SpanContextParseException {
+    assertThat(binaryFormat.fromByteArray(EXAMPLE_BYTES)).isEqualTo(EXAMPLE_SPAN_CONTEXT);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void fromBinaryValue_NullInput() throws SpanContextParseException {
+    binaryFormat.fromByteArray(null);
+  }
+
+  @Test
+  public void fromBinaryValue_EmptyInput() throws SpanContextParseException {
+    expectedException.expect(SpanContextParseException.class);
+    expectedException.expectMessage("Unsupported version.");
+    binaryFormat.fromByteArray(new byte[0]);
+  }
+
+  @Test
+  public void fromBinaryValue_UnsupportedVersionId() throws SpanContextParseException {
+    expectedException.expect(SpanContextParseException.class);
+    expectedException.expectMessage("Unsupported version.");
+    binaryFormat.fromByteArray(
+        new byte[] {
+          66, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 97, 98, 99, 100, 101,
+          102, 103, 104, 1
+        });
+  }
+
+  @Test
+  public void fromBinaryValue_UnsupportedFieldIdFirst() throws SpanContextParseException {
+    expectedException.expect(SpanContextParseException.class);
+    expectedException.expectMessage(
+        "Invalid input: expected trace ID at offset " + BinaryFormatImpl.TRACE_ID_FIELD_ID_OFFSET);
+    binaryFormat.fromByteArray(
+        new byte[] {
+          0, 4, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99, 100,
+          101, 102, 103, 104, 2, 1
+        });
+  }
+
+  @Test
+  public void fromBinaryValue_UnsupportedFieldIdSecond() throws SpanContextParseException {
+    expectedException.expect(SpanContextParseException.class);
+    expectedException.expectMessage(
+        "Invalid input: expected span ID at offset " + BinaryFormatImpl.SPAN_ID_FIELD_ID_OFFSET);
+    binaryFormat.fromByteArray(
+        new byte[] {
+          0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 3, 97, 98, 99, 100,
+          101, 102, 103, 104, 2, 1
+        });
+  }
+
+  @Test
+  public void fromBinaryValue_UnsupportedFieldIdThird_skipped() throws SpanContextParseException {
+    assertThat(
+            binaryFormat
+                .fromByteArray(
+                    new byte[] {
+                      0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97,
+                      98, 99, 100, 101, 102, 103, 104, 0, 1
+                    })
+                .isValid())
+        .isTrue();
+  }
+
+  @Test
+  public void fromBinaryValue_ShorterTraceId() throws SpanContextParseException {
+    expectedException.expect(SpanContextParseException.class);
+    expectedException.expectMessage("Invalid input: truncated");
+    binaryFormat.fromByteArray(
+        new byte[] {0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76});
+  }
+
+  @Test
+  public void fromBinaryValue_ShorterSpanId() throws SpanContextParseException {
+    expectedException.expect(SpanContextParseException.class);
+    expectedException.expectMessage("Invalid input: truncated");
+    binaryFormat.fromByteArray(new byte[] {0, 1, 97, 98, 99, 100, 101, 102, 103});
+  }
+
+  @Test
+  public void fromBinaryValue_ShorterTraceOptions() throws SpanContextParseException {
+    expectedException.expect(SpanContextParseException.class);
+    expectedException.expectMessage("Invalid input: truncated");
+    binaryFormat.fromByteArray(
+        new byte[] {
+          0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99, 100,
+          101, 102, 103, 104, 2
+        });
+  }
+
+  @Test
+  public void fromBinaryValue_MissingTraceOptionsOk() throws SpanContextParseException {
+    SpanContext extracted =
+        binaryFormat.fromByteArray(
+            new byte[] {
+              0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99,
+              100, 101, 102, 103, 104
+            });
+
+    assertThat(extracted.isValid()).isTrue();
+    assertThat(extracted.getTraceOptions()).isEqualTo(TraceOptions.DEFAULT);
+  }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/PropagationComponentImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/PropagationComponentImplTest.java
new file mode 100644
index 0000000..00ed90f
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/PropagationComponentImplTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.implcore.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.propagation.PropagationComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PropagationComponentImpl}. */
+@RunWith(JUnit4.class)
+public class PropagationComponentImplTest {
+  private final PropagationComponent propagationComponent = new PropagationComponentImpl();
+
+  @Test
+  public void implementationOfBinary() {
+    assertThat(propagationComponent.getBinaryFormat()).isInstanceOf(BinaryFormatImpl.class);
+  }
+
+  @Test
+  public void implementationOfB3Format() {
+    assertThat(propagationComponent.getB3Format()).isInstanceOf(B3Format.class);
+  }
+}
diff --git a/impl_lite/README.md b/impl_lite/README.md
new file mode 100644
index 0000000..ad7bb9b
--- /dev/null
+++ b/impl_lite/README.md
@@ -0,0 +1,6 @@
+OpenCensus Android implementation
+======================================================
+
+* Android compatible.
+* StatsManager specifies the stats implementation classes that should be used
+  with Android.
diff --git a/impl_lite/build.gradle b/impl_lite/build.gradle
new file mode 100644
index 0000000..b8692fd
--- /dev/null
+++ b/impl_lite/build.gradle
@@ -0,0 +1,12 @@
+description = 'OpenCensus Lite Implementation'
+
+dependencies {
+    compile project(':opencensus-api'),
+            project(':opencensus-impl-core')
+
+    testCompile project(':opencensus-api'),
+            project(':opencensus-impl-core')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/impl_lite/src/main/java/io/opencensus/impllite/metrics/MetricsComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/impllite/metrics/MetricsComponentImplLite.java
new file mode 100644
index 0000000..6161c12
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/impllite/metrics/MetricsComponentImplLite.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018, 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.impllite.metrics;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.metrics.MetricsComponentImplBase;
+import io.opencensus.metrics.MetricsComponent;
+
+/** Android-compatible implementation of {@link MetricsComponent}. */
+public final class MetricsComponentImplLite extends MetricsComponentImplBase {
+
+  public MetricsComponentImplLite() {
+    super(MillisClock.getInstance());
+  }
+}
diff --git a/impl_lite/src/main/java/io/opencensus/impllite/stats/StatsComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/impllite/stats/StatsComponentImplLite.java
new file mode 100644
index 0000000..a58a9d3
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/impllite/stats/StatsComponentImplLite.java
@@ -0,0 +1,31 @@
+/*
+ * 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.impllite.stats;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.stats.StatsComponentImplBase;
+import io.opencensus.stats.StatsComponent;
+
+/** Android-compatible implementation of {@link StatsComponent}. */
+public final class StatsComponentImplLite extends StatsComponentImplBase {
+
+  public StatsComponentImplLite() {
+    // TODO(sebright): Use a more efficient queue implementation.
+    super(new SimpleEventQueue(), MillisClock.getInstance());
+  }
+}
diff --git a/impl_lite/src/main/java/io/opencensus/impllite/tags/TagsComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/impllite/tags/TagsComponentImplLite.java
new file mode 100644
index 0000000..dc0d900
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/impllite/tags/TagsComponentImplLite.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.impllite.tags;
+
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.TagsComponent;
+
+/** Android-compatible implementation of {@link TagsComponent}. */
+public final class TagsComponentImplLite extends TagsComponentImplBase {}
diff --git a/impl_lite/src/main/java/io/opencensus/impllite/trace/TraceComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/impllite/trace/TraceComponentImplLite.java
new file mode 100644
index 0000000..8c06755
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/impllite/trace/TraceComponentImplLite.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.impllite.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.TraceComponentImplBase;
+import io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler;
+import io.opencensus.trace.TraceComponent;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/** Android-compatible implementation of the {@link TraceComponent}. */
+public final class TraceComponentImplLite extends TraceComponent {
+  private final TraceComponentImplBase traceComponentImplBase;
+
+  /** Public constructor to be used with reflection loading. */
+  public TraceComponentImplLite() {
+    traceComponentImplBase =
+        new TraceComponentImplBase(
+            MillisClock.getInstance(), new SecureRandomHandler(), new SimpleEventQueue());
+  }
+
+  @Override
+  public Tracer getTracer() {
+    return traceComponentImplBase.getTracer();
+  }
+
+  @Override
+  public PropagationComponent getPropagationComponent() {
+    return traceComponentImplBase.getPropagationComponent();
+  }
+
+  @Override
+  public Clock getClock() {
+    return traceComponentImplBase.getClock();
+  }
+
+  @Override
+  public ExportComponent getExportComponent() {
+    return traceComponentImplBase.getExportComponent();
+  }
+
+  @Override
+  public TraceConfig getTraceConfig() {
+    return traceComponentImplBase.getTraceConfig();
+  }
+}
diff --git a/impl_lite/src/main/java/io/opencensus/trace/TraceComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/trace/TraceComponentImplLite.java
new file mode 100644
index 0000000..5e80b93
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/trace/TraceComponentImplLite.java
@@ -0,0 +1,66 @@
+/*
+ * 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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.TraceComponentImplBase;
+import io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/** Android-compatible implementation of the {@link TraceComponent}. */
+// TraceComponentImplLite was moved to io.opencensus.impllite.trace. This class exists for backwards
+// compatibility, so that it can be loaded by opencensus-api 0.5.
+@Deprecated
+public final class TraceComponentImplLite extends TraceComponent {
+  private final TraceComponentImplBase traceComponentImplBase;
+
+  /** Public constructor to be used with reflection loading. */
+  public TraceComponentImplLite() {
+    traceComponentImplBase =
+        new TraceComponentImplBase(
+            MillisClock.getInstance(), new SecureRandomHandler(), new SimpleEventQueue());
+  }
+
+  @Override
+  public Tracer getTracer() {
+    return traceComponentImplBase.getTracer();
+  }
+
+  @Override
+  public PropagationComponent getPropagationComponent() {
+    return traceComponentImplBase.getPropagationComponent();
+  }
+
+  @Override
+  public Clock getClock() {
+    return traceComponentImplBase.getClock();
+  }
+
+  @Override
+  public ExportComponent getExportComponent() {
+    return traceComponentImplBase.getExportComponent();
+  }
+
+  @Override
+  public TraceConfig getTraceConfig() {
+    return traceComponentImplBase.getTraceConfig();
+  }
+}
diff --git a/impl_lite/src/test/java/io/opencensus/impllite/metrics/MetricsTest.java b/impl_lite/src/test/java/io/opencensus/impllite/metrics/MetricsTest.java
new file mode 100644
index 0000000..7ee900a
--- /dev/null
+++ b/impl_lite/src/test/java/io/opencensus/impllite/metrics/MetricsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018, 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.impllite.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.metrics.MetricRegistryImpl;
+import io.opencensus.implcore.metrics.export.ExportComponentImpl;
+import io.opencensus.metrics.Metrics;
+import io.opencensus.metrics.MetricsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link MetricsComponent} through the {@link Metrics} class. */
+@RunWith(JUnit4.class)
+public class MetricsTest {
+
+  @Test
+  public void getExportComponent() {
+    assertThat(Metrics.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+  }
+
+  @Test
+  public void getMetricRegistry() {
+    assertThat(Metrics.getMetricRegistry()).isInstanceOf(MetricRegistryImpl.class);
+  }
+}
diff --git a/impl_lite/src/test/java/io/opencensus/impllite/stats/StatsTest.java b/impl_lite/src/test/java/io/opencensus/impllite/stats/StatsTest.java
new file mode 100644
index 0000000..313f891
--- /dev/null
+++ b/impl_lite/src/test/java/io/opencensus/impllite/stats/StatsTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016-17, 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.impllite.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.stats.StatsRecorderImpl;
+import io.opencensus.implcore.stats.ViewManagerImpl;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link StatsComponent} through the {@link Stats} class. */
+@RunWith(JUnit4.class)
+public final class StatsTest {
+  @Test
+  public void getStatsRecorder() {
+    assertThat(Stats.getStatsRecorder()).isInstanceOf(StatsRecorderImpl.class);
+  }
+
+  @Test
+  public void getViewManager() {
+    assertThat(Stats.getViewManager()).isInstanceOf(ViewManagerImpl.class);
+  }
+}
diff --git a/impl_lite/src/test/java/io/opencensus/impllite/tags/TagsTest.java b/impl_lite/src/test/java/io/opencensus/impllite/tags/TagsTest.java
new file mode 100644
index 0000000..890cdb1
--- /dev/null
+++ b/impl_lite/src/test/java/io/opencensus/impllite/tags/TagsTest.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.impllite.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.tags.TaggerImpl;
+import io.opencensus.implcore.tags.propagation.TagPropagationComponentImpl;
+import io.opencensus.tags.Tags;
+import io.opencensus.tags.TagsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link TagsComponent} through the {@link Tags} class. */
+@RunWith(JUnit4.class)
+public final class TagsTest {
+  @Test
+  public void getTagger() {
+    assertThat(Tags.getTagger()).isInstanceOf(TaggerImpl.class);
+  }
+
+  @Test
+  public void getTagContextSerializer() {
+    assertThat(Tags.getTagPropagationComponent()).isInstanceOf(TagPropagationComponentImpl.class);
+  }
+}
diff --git a/impl_lite/src/test/java/io/opencensus/impllite/trace/TraceComponentImplLiteTest.java b/impl_lite/src/test/java/io/opencensus/impllite/trace/TraceComponentImplLiteTest.java
new file mode 100644
index 0000000..c4a609a
--- /dev/null
+++ b/impl_lite/src/test/java/io/opencensus/impllite/trace/TraceComponentImplLiteTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.impllite.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.trace.TracerImpl;
+import io.opencensus.implcore.trace.export.ExportComponentImpl;
+import io.opencensus.implcore.trace.propagation.PropagationComponentImpl;
+import io.opencensus.trace.Tracing;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceComponentImplLite}. */
+@RunWith(JUnit4.class)
+public class TraceComponentImplLiteTest {
+  @Test
+  public void implementationOfTracer() {
+    assertThat(Tracing.getTracer()).isInstanceOf(TracerImpl.class);
+  }
+
+  @Test
+  public void implementationOfBinaryPropagationHandler() {
+    assertThat(Tracing.getPropagationComponent()).isInstanceOf(PropagationComponentImpl.class);
+  }
+
+  @Test
+  public void implementationOfClock() {
+    assertThat(Tracing.getClock()).isInstanceOf(MillisClock.class);
+  }
+
+  @Test
+  public void implementationOfTraceExporter() {
+    assertThat(Tracing.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+  }
+}
diff --git a/scripts/check-git-history.py b/scripts/check-git-history.py
new file mode 100644
index 0000000..df13e42
--- /dev/null
+++ b/scripts/check-git-history.py
@@ -0,0 +1,51 @@
+import os
+import sys
+import traceback
+
+def main(argv):
+  # Only check the history if the build is running on a pull request.
+  # Build could be running on pull request using travis or kokoro.
+  if is_travis_pull_request() or is_kokoro_presubmit_request():
+    # This function assumes that HEAD^1 is the base branch and HEAD^2 is the
+    # pull request.
+    exit_if_pull_request_has_merge_commits()
+    print 'Checked pull request history: SUCCEEDED'
+  else:
+    print 'Skipped history check.'
+
+def is_kokoro_presubmit_request():
+  '''Returns true if KOKORO_GITHUB_PULL_REQUEST_NUMBER is set.'''
+  if 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' in os.environ:
+    return True
+  return False
+
+def is_travis_pull_request():
+  '''Returns true if TRAVIS_PULL_REQUEST is set to indicate a pull request.'''
+  if 'TRAVIS_PULL_REQUEST' in os.environ:
+    return os.environ['TRAVIS_PULL_REQUEST'] != 'false'
+  return False
+
+def exit_if_pull_request_has_merge_commits():
+  '''Exits with an error if any of the commits added by the pull request are
+     merge commits.'''
+  # Print the parents of each commit added by the pull request.
+  git_command = 'git log --format="%P" HEAD^1..HEAD^2'
+  for line in os.popen(git_command):
+    parents = line.split()
+    assert len(parents) >= 1, line
+    if len(parents) > 1:
+      print 'Pull request contains a merge commit:'
+      print_history()
+      print 'Checked pull request history: FAILED'
+      sys.exit(1)
+
+def print_history():
+  os.system('git log HEAD^1 HEAD^2 -30 --graph --oneline --decorate')
+
+def read_process(command):
+  '''Runs a command and returns everything printed to stdout.'''
+  with os.popen(command, 'r') as fd:
+    return fd.read()
+
+if __name__ == '__main__':
+  main(sys.argv)
diff --git a/scripts/travis_script b/scripts/travis_script
new file mode 100755
index 0000000..7b7bec5
--- /dev/null
+++ b/scripts/travis_script
@@ -0,0 +1,78 @@
+#!/bin/bash
+#
+# Travis build script, cf.
+# https://docs.travis-ci.com/user/customizing-the-build/#Implementing-Complex-Build-Steps.
+
+set -o errexit
+set -o xtrace
+
+case "$TASK" in
+  "CHECK_GIT_HISTORY")
+    python "$(dirname "$0")"/check-git-history.py
+    ;;
+  "BUILD")
+    case "$TRAVIS_OS_NAME" in
+      "linux")
+        source /opt/jdk_switcher/jdk_switcher.sh
+        export JAVA8_HOME="$(jdk_switcher home oraclejdk8)"
+        case "$TRAVIS_JDK_VERSION" in
+          "oraclejdk9")
+            ./gradlew clean assemble check --stacktrace
+            ;;
+          "oraclejdk8")
+            export JAVA_HOMES="$(jdk_switcher home openjdk6)/jre:$(jdk_switcher home openjdk7)/jre:$(jdk_switcher home oraclejdk8)/jre:$(jdk_switcher home oraclejdk9)"
+            ./gradlew clean assemble --stacktrace
+            ./gradlew check :opencensus-all:jacocoTestReport
+            ./gradlew verGJF
+            ;;
+          "openjdk7")
+            # "./gradlew classes testClasses" is a workaround for
+            # https://github.com/gradle/gradle/issues/2421.
+            # See https://github.com/gradle/gradle/issues/2421#issuecomment-319916874.
+            JAVA_HOME="$(jdk_switcher home openjdk8)" ./gradlew classes testClasses
+            ./gradlew clean assemble --stacktrace
+            ./gradlew check
+            ;;
+          *)
+            echo "Unknown JDK version $TRAVIS_JDK_VERSION"
+            exit 1
+            ;;
+        esac
+        ;;
+      "osx")
+        # OS X is a separate case, because the JDK version is determined by the OS X image:
+        # https://docs.travis-ci.com/user/reference/osx/#JDK-and-OS-X
+        ./gradlew clean assemble --stacktrace
+        ./gradlew check
+        ;;
+      *)
+        echo "Unknown OS name $TRAVIS_OS_NAME"
+        exit 1
+        ;;
+    esac
+    ;;
+  "CHECKER_FRAMEWORK")
+    ./gradlew clean assemble -PcheckerFramework=true
+    ;;
+  "CHECK_EXAMPLES_LICENSE")
+    curl -L -o checkstyle-8.12-all.jar https://github.com/checkstyle/checkstyle/releases/download/checkstyle-8.12/checkstyle-8.12-all.jar
+    java -DrootDir=. -jar checkstyle-8.12-all.jar -c buildscripts/checkstyle.xml examples/src/
+    ;;
+  "CHECK_EXAMPLES_FORMAT")
+    curl -L -o google-java-format-1.5-all-deps.jar https://github.com/google/google-java-format/releases/download/google-java-format-1.5/google-java-format-1.5-all-deps.jar
+    java -jar google-java-format-1.5-all-deps.jar --set-exit-if-changed --dry-run `find examples/src/ -name '*.java'`
+    ;;
+  "BUILD_EXAMPLES_GRADLE")
+    pushd examples && ./gradlew clean assemble --stacktrace && popd
+    ;;
+  "BUILD_EXAMPLES_MAVEN")
+    pushd examples && mvn clean package appassembler:assemble -e && popd
+    ;;
+  "BUILD_EXAMPLES_BAZEL")
+    pushd examples && bazel clean && bazel build :all && popd
+    ;;
+  *)
+    echo "Unknown task $TASK"
+    exit 1
+    ;;
+esac
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..7c224ed
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,79 @@
+rootProject.name = "opencensus-java"
+
+include ":opencensus-api"
+include ":opencensus-impl-core"
+include ":opencensus-impl-lite"
+include ":opencensus-impl"
+include ":opencensus-testing"
+include ":opencensus-exporter-trace-instana"
+include ":opencensus-exporter-trace-logging"
+include ":opencensus-exporter-trace-ocagent"
+include ":opencensus-exporter-trace-stackdriver"
+include ":opencensus-exporter-trace-zipkin"
+include ":opencensus-exporter-trace-jaeger"
+include ":opencensus-exporter-stats-signalfx"
+include ":opencensus-exporter-stats-stackdriver"
+include ":opencensus-exporter-stats-prometheus"
+include ":opencensus-contrib-agent"
+include ":opencensus-contrib-appengine-standard-util"
+include ":opencensus-contrib-dropwizard"
+include ":opencensus-contrib-exemplar-util"
+include ":opencensus-contrib-grpc-metrics"
+include ":opencensus-contrib-grpc-util"
+include ":opencensus-contrib-http-util"
+include ":opencensus-contrib-log-correlation-log4j2"
+include ":opencensus-contrib-log-correlation-stackdriver"
+include ":opencensus-contrib-monitored-resource-util"
+include ":opencensus-contrib-spring"
+include ":opencensus-contrib-spring-sleuth-v1x"
+
+project(':opencensus-api').projectDir = "$rootDir/api" as File
+project(':opencensus-impl-core').projectDir = "$rootDir/impl_core" as File
+project(':opencensus-impl-lite').projectDir = "$rootDir/impl_lite" as File
+project(':opencensus-impl').projectDir = "$rootDir/impl" as File
+project(':opencensus-testing').projectDir = "$rootDir/testing" as File
+project(':opencensus-contrib-agent').projectDir = "$rootDir/contrib/agent" as File
+project(':opencensus-contrib-appengine-standard-util').projectDir =
+        "$rootDir/contrib/appengine_standard_util" as File
+project(':opencensus-contrib-dropwizard').projectDir = "$rootDir/contrib/dropwizard" as File
+project(':opencensus-contrib-exemplar-util').projectDir = "$rootDir/contrib/exemplar_util" as File
+project(':opencensus-contrib-grpc-metrics').projectDir = "$rootDir/contrib/grpc_metrics" as File
+project(':opencensus-contrib-grpc-util').projectDir = "$rootDir/contrib/grpc_util" as File
+project(':opencensus-contrib-http-util').projectDir = "$rootDir/contrib/http_util" as File
+project(':opencensus-contrib-log-correlation-log4j2').projectDir =
+        "$rootDir/contrib/log_correlation/log4j2" as File
+project(':opencensus-contrib-log-correlation-stackdriver').projectDir =
+        "$rootDir/contrib/log_correlation/stackdriver" as File
+project(':opencensus-contrib-monitored-resource-util').projectDir =
+        "$rootDir/contrib/monitored_resource_util" as File
+project(':opencensus-contrib-spring').projectDir = "$rootDir/contrib/spring" as File
+project(':opencensus-contrib-spring-sleuth-v1x').projectDir =
+        "$rootDir/contrib/spring_sleuth_v1x" as File
+project(':opencensus-exporter-stats-signalfx').projectDir =
+        "$rootDir/exporters/stats/signalfx" as File
+project(':opencensus-exporter-stats-stackdriver').projectDir =
+        "$rootDir/exporters/stats/stackdriver" as File
+project(':opencensus-exporter-stats-prometheus').projectDir =
+        "$rootDir/exporters/stats/prometheus" as File
+project(':opencensus-exporter-trace-instana').projectDir =
+        "$rootDir/exporters/trace/instana" as File
+project(':opencensus-exporter-trace-logging').projectDir =
+        "$rootDir/exporters/trace/logging" as File
+project(':opencensus-exporter-trace-ocagent').projectDir =
+        "$rootDir/exporters/trace/ocagent" as File
+project(':opencensus-exporter-trace-stackdriver').projectDir =
+        "$rootDir/exporters/trace/stackdriver" as File
+project(':opencensus-exporter-trace-zipkin').projectDir = "$rootDir/exporters/trace/zipkin" as File
+project(':opencensus-exporter-trace-jaeger').projectDir = "$rootDir/exporters/trace/jaeger" as File
+
+
+// Java8 projects only
+if (JavaVersion.current().isJava8Compatible()) {
+    include ":opencensus-all"
+    include ":opencensus-benchmarks"
+    include ":opencensus-contrib-zpages"
+
+    project(':opencensus-all').projectDir = "$rootDir/all" as File
+    project(':opencensus-benchmarks').projectDir = "$rootDir/benchmarks" as File
+    project(':opencensus-contrib-zpages').projectDir = "$rootDir/contrib/zpages" as File
+}
diff --git a/testing/README.md b/testing/README.md
new file mode 100644
index 0000000..268bbf3
--- /dev/null
+++ b/testing/README.md
@@ -0,0 +1,5 @@
+OpenCensus Testing Package
+======================================================
+
+* Java 6 and Android compatible.
+* The classes in this directory can be used to test the API integration.
diff --git a/testing/build.gradle b/testing/build.gradle
new file mode 100644
index 0000000..811b059
--- /dev/null
+++ b/testing/build.gradle
@@ -0,0 +1,11 @@
+description = 'OpenCensus Testing'
+
+dependencies {
+    compile project(':opencensus-api'),
+            libraries.guava
+
+    testCompile project(':opencensus-api')
+
+    signature "org.codehaus.mojo.signature:java17:1.0@signature"
+    signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/testing/src/main/java/io/opencensus/testing/common/TestClock.java b/testing/src/main/java/io/opencensus/testing/common/TestClock.java
new file mode 100644
index 0000000..b670cb7
--- /dev/null
+++ b/testing/src/main/java/io/opencensus/testing/common/TestClock.java
@@ -0,0 +1,104 @@
+/*
+ * 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.testing.common;
+
+import com.google.common.math.LongMath;
+import io.opencensus.common.Clock;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link Clock} that allows the time to be set for testing.
+ *
+ * @since 0.5
+ */
+@ThreadSafe
+public final class TestClock extends Clock {
+  private static final int NUM_NANOS_PER_SECOND = 1000 * 1000 * 1000;
+
+  @GuardedBy("this")
+  private Timestamp currentTime = validateNanos(Timestamp.create(1493419949, 223123456));
+
+  private TestClock() {}
+
+  /**
+   * Creates a clock initialized to a constant non-zero time. {@code Timestamp.create(0, 0)} is not
+   * a good default, because it represents an invalid time.
+   *
+   * @return a clock initialized to a constant non-zero time.
+   * @since 0.5
+   */
+  public static TestClock create() {
+    return new TestClock();
+  }
+
+  /**
+   * Creates a clock with the given time.
+   *
+   * @param time the initial time.
+   * @return a new {@code TestClock} with the given time.
+   * @since 0.5
+   */
+  public static TestClock create(Timestamp time) {
+    TestClock clock = new TestClock();
+    clock.setTime(time);
+    return clock;
+  }
+
+  /**
+   * Sets the time.
+   *
+   * @param time the new time.
+   * @since 0.5
+   */
+  public synchronized void setTime(Timestamp time) {
+    currentTime = validateNanos(time);
+  }
+
+  /**
+   * Advances the time by a duration.
+   *
+   * @param duration the increase in time.
+   * @since 0.5
+   */
+  public synchronized void advanceTime(Duration duration) {
+    currentTime = validateNanos(currentTime.addDuration(duration));
+  }
+
+  @Override
+  public synchronized Timestamp now() {
+    return currentTime;
+  }
+
+  @Override
+  public synchronized long nowNanos() {
+    return getNanos(currentTime);
+  }
+
+  private static Timestamp validateNanos(Timestamp time) {
+    getNanos(time);
+    return time;
+  }
+
+  // Converts Timestamp into nanoseconds since time 0 and throws an exception if it overflows.
+  private static long getNanos(Timestamp time) {
+    return LongMath.checkedAdd(
+        LongMath.checkedMultiply(time.getSeconds(), NUM_NANOS_PER_SECOND), time.getNanos());
+  }
+}
diff --git a/testing/src/main/java/io/opencensus/testing/export/TestHandler.java b/testing/src/main/java/io/opencensus/testing/export/TestHandler.java
new file mode 100644
index 0000000..6d73aff
--- /dev/null
+++ b/testing/src/main/java/io/opencensus/testing/export/TestHandler.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.testing.export;
+
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * A {@link SpanExporter.Handler} for testing only.
+ *
+ * @since 0.9
+ */
+public final class TestHandler extends SpanExporter.Handler {
+
+  private final Object monitor = new Object();
+
+  // TODO: Decide whether to use a different class instead of LinkedList.
+  @GuardedBy("monitor")
+  @SuppressWarnings("JdkObsolete")
+  private final List<SpanData> spanDataList = new LinkedList<SpanData>();
+
+  @Override
+  public void export(Collection<SpanData> spanDataList) {
+    synchronized (monitor) {
+      this.spanDataList.addAll(spanDataList);
+      monitor.notifyAll();
+    }
+  }
+
+  /**
+   * Waits until we received numberOfSpans spans to export. Returns the list of exported {@link
+   * SpanData} objects, otherwise {@code null} if the current thread is interrupted.
+   *
+   * @param numberOfSpans the number of minimum spans to be collected.
+   * @return the list of exported {@link SpanData} objects, otherwise {@code null} if the current
+   *     thread is interrupted.
+   * @since 0.9
+   */
+  @Nullable
+  public List<SpanData> waitForExport(int numberOfSpans) {
+    List<SpanData> ret;
+    synchronized (monitor) {
+      while (spanDataList.size() < numberOfSpans) {
+        try {
+          monitor.wait();
+        } catch (InterruptedException e) {
+          // Preserve the interruption status as per guidance.
+          Thread.currentThread().interrupt();
+          return null;
+        }
+      }
+      ret = new ArrayList<SpanData>(spanDataList);
+      spanDataList.clear();
+    }
+    return ret;
+  }
+}
diff --git a/testing/src/test/java/io/opencensus/testing/common/TestClockTest.java b/testing/src/test/java/io/opencensus/testing/common/TestClockTest.java
new file mode 100644
index 0000000..24cd7fc
--- /dev/null
+++ b/testing/src/test/java/io/opencensus/testing/common/TestClockTest.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.testing.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TestClock}. */
+@RunWith(JUnit4.class)
+public final class TestClockTest {
+  private static final int NUM_NANOS_PER_SECOND = 1000 * 1000 * 1000;
+
+  @Test
+  public void setAndGetTime() {
+    TestClock clock = TestClock.create(Timestamp.create(1, 2));
+    assertThat(clock.now()).isEqualTo(Timestamp.create(1, 2));
+    clock.setTime(Timestamp.create(3, 4));
+    assertThat(clock.now()).isEqualTo(Timestamp.create(3, 4));
+  }
+
+  @Test
+  public void advanceTime() {
+    TestClock clock = TestClock.create(Timestamp.create(1, 500 * 1000 * 1000));
+    clock.advanceTime(Duration.create(2, 600 * 1000 * 1000));
+    assertThat(clock.now()).isEqualTo(Timestamp.create(4, 100 * 1000 * 1000));
+  }
+
+  @Test
+  public void measureElapsedTime() {
+    TestClock clock = TestClock.create(Timestamp.create(10, 1));
+    long nanos1 = clock.nowNanos();
+    clock.setTime(Timestamp.create(11, 5));
+    long nanos2 = clock.nowNanos();
+    assertThat(nanos2 - nanos1).isEqualTo(1000 * 1000 * 1000 + 4);
+  }
+
+  @Test(expected = ArithmeticException.class)
+  public void catchOverflow() {
+    TestClock.create(Timestamp.create(Long.MAX_VALUE / NUM_NANOS_PER_SECOND + 1, 0));
+  }
+
+  @Test(expected = ArithmeticException.class)
+  public void catchNegativeOverflow() {
+    TestClock.create(Timestamp.create(Long.MIN_VALUE / NUM_NANOS_PER_SECOND - 1, 0));
+  }
+}