diff options
author | Julien Desprez <jdesprez@google.com> | 2018-10-19 11:11:10 -0700 |
---|---|---|
committer | Julien Desprez <jdesprez@google.com> | 2018-10-19 11:11:34 -0700 |
commit | dd3cabeacc5c8079b0d8674230819f3c54dca590 (patch) | |
tree | ede84fcf0a9687d4907ae5f8a4788271d62e0922 | |
parent | cfbefd32336596ea63784607e4106dc37ce0567f (diff) | |
parent | 633fde4378905bffb967b30857257427cced4228 (diff) | |
download | opencensus-java-dd3cabeacc5c8079b0d8674230819f3c54dca590.tar.gz |
Merge remote-tracking branch 'aosp/upstream-master' into mergeandroid-wear-8.0.0_r2
Test: None
Change-Id: I8a34a16c1ec7981a9cc93758e216ae54bac986f6
628 files changed, 76952 insertions, 0 deletions
diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 00000000..7d484c62 --- /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 00000000..b5caa381 --- /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 00000000..e69de29b --- /dev/null +++ b/.gitmodules diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..996d4c01 --- /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 00000000..e068e731 --- /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 00000000..352c2419 --- /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 00000000..91279cc7 --- /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 00000000..d6456956 --- /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 00000000..0859e7db --- /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 00000000..649ac81f --- /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 00000000..83ffb69e --- /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 00000000..83891d4d --- /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 00000000..31274ca0 --- /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 00000000..cd311935 --- /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 00000000..f46cd187 --- /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 00000000..7a4da7c7 --- /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 00000000..a9ed5a9e --- /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 00000000..ea3457ca --- /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 00000000..d84fba20 --- /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 00000000..30d07ac7 --- /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 00000000..3f659c12 --- /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 00000000..de954f50 --- /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 00000000..42efa1f2 --- /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 00000000..2332733c --- /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 00000000..024a93f8 --- /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 00000000..ff3cfda9 --- /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 00000000..db119e2e --- /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 00000000..d17b3fd8 --- /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 00000000..6ace2f7c --- /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 00000000..cd2b68ed --- /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 00000000..1ebfd7cf --- /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 00000000..e90a6573 --- /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 00000000..f4a8da07 --- /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 00000000..8cfb7294 --- /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 00000000..717e333c --- /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 00000000..df5c9840 --- /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 00000000..fda13e9e --- /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 00000000..5dd35b23 --- /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 00000000..3aaca153 --- /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 00000000..621873f9 --- /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 00000000..32759973 --- /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 00000000..efc51e64 --- /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 00000000..e5708655 --- /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 00000000..1d4489c9 --- /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 00000000..5be15594 --- /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 00000000..920a4a88 --- /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 00000000..3a992306 --- /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 00000000..d55f101c --- /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 00000000..11e1fdbd --- /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 00000000..7b93fc86 --- /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 00000000..a4629f8e --- /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 00000000..739a0a9f --- /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 00000000..304d9294 --- /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 00000000..1f382f9b --- /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 00000000..c82ca961 --- /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 00000000..bfaeae98 --- /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 00000000..00a939c0 --- /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 00000000..33eadf0c --- /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 00000000..9c95e847 --- /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 00000000..c6e12b67 --- /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 00000000..61e21e6c --- /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 00000000..2de7fd70 --- /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 00000000..beb84f06 --- /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 00000000..647a667d --- /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 00000000..e7e94a38 --- /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 00000000..8393f631 --- /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 00000000..6b2f2409 --- /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 00000000..9764fce5 --- /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 00000000..87b8c8b0 --- /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 00000000..f563ff9a --- /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 00000000..df6edaa7 --- /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 00000000..a00165cc --- /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 00000000..981daa0e --- /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 00000000..944122e1 --- /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 00000000..fb52b164 --- /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 00000000..9e0a7a82 --- /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 00000000..e36acdff --- /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 00000000..f4268968 --- /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 00000000..ca4582bd --- /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 00000000..9111ca28 --- /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 00000000..f1e203ad --- /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 00000000..88970361 --- /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 00000000..07123647 --- /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 00000000..d34f1951 --- /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 00000000..eb19ee77 --- /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 00000000..39eb8cee --- /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 00000000..11dcb59f --- /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 00000000..bb3c9b74 --- /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 00000000..6ececa79 --- /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 00000000..8936bbbb --- /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 00000000..97f2fdd2 --- /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 00000000..efa9d1df --- /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 00000000..5ad961f6 --- /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 00000000..af6456d3 --- /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 00000000..aa2f055a --- /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 00000000..b0d9a470 --- /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 00000000..1de79710 --- /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 00000000..bca95868 --- /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 00000000..4b693aaa --- /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 00000000..722029e5 --- /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 00000000..e89af89b --- /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 00000000..8f8253b4 --- /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 00000000..f3a436a6 --- /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 00000000..49ed751b --- /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 00000000..c43fa6b0 --- /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 00000000..1fa85085 --- /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 00000000..d98d0f9e --- /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 00000000..465e4d4a --- /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 00000000..218f4dab --- /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 00000000..a2c0a239 --- /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 00000000..dae587c8 --- /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 00000000..f55cd775 --- /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 00000000..ff701e20 --- /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 00000000..ff70f52d --- /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 00000000..c334c5a6 --- /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 00000000..fac3c855 --- /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 00000000..5d00a45d --- /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 >= 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 00000000..f4dd4682 --- /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 00000000..73ac5265 --- /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 00000000..9d22a1c6 --- /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 00000000..77f39aba --- /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 00000000..7e875fd6 --- /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 00000000..a90f0419 --- /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 00000000..80d42af5 --- /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 00000000..d52e71f1 --- /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 00000000..7b61e235 --- /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 00000000..c6de645a --- /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 00000000..b9c18e00 --- /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 00000000..c10610a0 --- /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 00000000..3f4b9889 --- /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 00000000..ea636ca0 --- /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 00000000..55d58d4d --- /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 00000000..6db14a79 --- /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 00000000..ed786f6c --- /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 00000000..620bbb4f --- /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 00000000..d6228566 --- /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 00000000..b193b3c8 --- /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 00000000..1f4c33fa --- /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 00000000..5e866940 --- /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 00000000..608a8fe0 --- /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 00000000..dbae3c49 --- /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 00000000..6a462881 --- /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 00000000..b0cdea7c --- /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 00000000..83f2b59a --- /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 00000000..e5526b2f --- /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 00000000..eedb287c --- /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 00000000..d8a26cc8 --- /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 00000000..1c4e70f7 --- /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 00000000..9e0eee1f --- /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 00000000..85b31498 --- /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 00000000..15c6e883 --- /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 00000000..502170c6 --- /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 00000000..1025427f --- /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 00000000..ed205289 --- /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 00000000..cdfc7792 --- /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 00000000..c10df043 --- /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 00000000..92a2c8cf --- /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 00000000..bf947692 --- /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 00000000..a6d6d1de --- /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 00000000..cf337030 --- /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 00000000..36f2edb4 --- /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 00000000..a9302425 --- /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 00000000..4bae14a6 --- /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 00000000..44c7626f --- /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 00000000..4219173a --- /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 00000000..0120ffea --- /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 00000000..afba1bc0 --- /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 00000000..65482de1 --- /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 00000000..db07520e --- /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 00000000..025c7bae --- /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 00000000..48cf9fd4 --- /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 00000000..3c899e65 --- /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 00000000..9aa42c8c --- /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 00000000..dee517b6 --- /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 00000000..750d5d45 --- /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 00000000..54e9fab6 --- /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 00000000..c35c5dc4 --- /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 00000000..0db5d93c --- /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 00000000..05ef43c0 --- /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 00000000..185a5acd --- /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 00000000..6b16c3d0 --- /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 00000000..b6ab8e0e --- /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 00000000..5c1ebf5d --- /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 00000000..3444d2b3 --- /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 00000000..fde32fe6 --- /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 00000000..8c132377 --- /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 00000000..a21a8aac --- /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 00000000..839c8945 --- /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 00000000..54e188c8 --- /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 00000000..4a5bc2ae --- /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 00000000..f7546ca4 --- /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 00000000..108db2d2 --- /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 00000000..1c3f07d5 --- /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 00000000..c8b5dc8f --- /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 00000000..3c46d097 --- /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 00000000..58dd4bbe --- /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 00000000..3374eb75 --- /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 00000000..e7c93a95 --- /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 00000000..d48e0894 --- /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 00000000..bdf07d53 --- /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 00000000..d7f385d0 --- /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 00000000..960da27c --- /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 00000000..6e9c7b0f --- /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 00000000..b991d145 --- /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 00000000..4f8c8508 --- /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 00000000..64544ffe --- /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 00000000..ba64e98e --- /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 00000000..92efb35d --- /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 00000000..c2e6e127 --- /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 00000000..7a46e97a --- /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 00000000..34493a90 --- /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 00000000..e591a8d6 --- /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 00000000..04688dd3 --- /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 00000000..e917817a --- /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 00000000..992937a1 --- /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 00000000..02f77f5f --- /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 00000000..dcb006ce --- /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 00000000..27bac1a2 --- /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 00000000..50b146e7 --- /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 00000000..a2c8d611 --- /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 00000000..d545878a --- /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 00000000..0d9e253b --- /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 00000000..e8aa21be --- /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 00000000..ddd15937 --- /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 00000000..3f4c872e --- /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 00000000..6f9a3dc1 --- /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 00000000..7c14df77 --- /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 00000000..19cc67d0 --- /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 00000000..98f4a3b9 --- /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 00000000..112fc206 --- /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 00000000..5677835a --- /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 00000000..bb1281b4 --- /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 00000000..fe3a9803 --- /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 00000000..7787df07 --- /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 00000000..e5ff9b08 --- /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 00000000..f8581eba --- /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 00000000..42ed251e --- /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 00000000..20b3240e --- /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 00000000..61d2fa11 --- /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 00000000..9497f6f2 --- /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 00000000..f24c28a2 --- /dev/null +++ b/contrib/agent/README.md @@ -0,0 +1,95 @@ +# OpenCensus Agent for Java + +[![Build Status][travis-image]][travis-url] +[![Windows Build Status][appveyor-image]][appveyor-url] +[![Maven Central][maven-image]][maven-url] + +The *OpenCensus Agent for Java* collects and sends latency data about your Java process to +OpenCensus backends such as Zipkin, Stackdriver Trace, etc. for analysis and visualization. + + +## Features + +The *OpenCensus Agent for Java* is in an early development stage. The following features are +currently implemented: + +TODO(stschmidt): Update README.md along with implementation. + + +### Automatic context propagation for Executors + +The context of the caller of [Executor#execute](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html#execute-java.lang.Runnable-) +is automatically propagated to the submitted Runnable. + + +### Automatic context propagation for Threads + +The context of the caller of [Thread#start](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#start--) +is automatically propagated to the new thread. + + +### Preliminary support for tracing + +As a proof-of-concept, the agent wraps the execution of +[URL#getContent](https://docs.oracle.com/javase/8/docs/api/java/net/URL.html#getContent--) in a new +trace span. + + +## Design Ideas + +We see tracing as a cross-cutting concern which the *OpenCensus Agent for Java* weaves into +existing Java bytecode (the application and its libraries) at runtime, typically when first loading +the concerned bytecode. + +This approach allows us to instrument arbitrary code without having to touch the source code of the +application or its dependencies. Furthermore, we don't require the application owner to upgrade any +of the application's third-party dependencies to specific versions. As long as the interface (e.g. +[java.sql.Driver#connect](https://docs.oracle.com/javase/8/docs/api/java/sql/Driver.html#connect-java.lang.String-java.util.Properties-)) +stays as-is across the supported versions, the Java agent's bytecode weaver will be able to +instrument the code. + +The *OpenCensus Agent for Java* uses [Byte Buddy](http://bytebuddy.net/), a widely used and +well-maintained bytecode manipulation library, for instrumenting selected Java methods at class +load-time. Which Java methods we want to intercept/instrument obviously depends on the library +(MongoDB vs. Redis, etc.) and the application. + + +## Installation and Usage + +Download the latest version of the *OpenCensus Agent for Java* `.jar` file +from [Maven Central][maven-url]. Store it somewhere on disk. + +To enable the *OpenCensus Agent for Java* for your application, add the option +`-javaagent:path/to/opencensus-contrib-agent-X.Y.Z.jar` to the invocation of the `java` +executable as shown in the following example. Replace `X.Y.Z` with the actual version number. + +```shell +java -javaagent:path/to/opencensus-contrib-agent-X.Y.Z.jar ... +``` + + +## Configuration + +The *OpenCensus Agent for Java* uses [Typesafe's configuration +library](https://lightbend.github.io/config/) for all user-configurable settings. Please refer to +[reference.conf](src/main/resources/reference.conf) for the available configuration knobs and their +defaults. + +You can override the default configuration in [different +ways](https://github.com/lightbend/config/blob/7cae92d3ae3ff9d06f1db43800232d2f73c6fe44/README.md#standard-behavior). +For example, to disable the automatic context propagation for Executors, add a system property as +follows: + +```shell +java -javaagent:path/to/opencensus-contrib-agent-X.Y.Z.jar \ + -Dopencensus.contrib.agent.context-propagation.executor.enabled=false \ + ... +``` + + +[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master +[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java +[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true +[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master +[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-agent/badge.svg +[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-agent diff --git a/contrib/agent/build.gradle b/contrib/agent/build.gradle new file mode 100644 index 00000000..11271a42 --- /dev/null +++ b/contrib/agent/build.gradle @@ -0,0 +1,246 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '2.0.2' +} + +description = 'OpenCensus Agent' + +def agentPackage = 'io.opencensus.contrib.agent' +def agentMainClass = "${agentPackage}.AgentMain" + +// The package containing the classes that need to be loaded by the bootstrap classloader because +// they are used from classes loaded by the bootstrap classloader. +def agentBootstrapPackage = "${agentPackage}.bootstrap" +def agentBootstrapPackageDir = agentBootstrapPackage.replace('.', '/') + '/' +def agentBootstrapClasses = agentBootstrapPackageDir + '**' + +// The package to which we relocate all third party packages. This avoids any conflicts of the +// agent's classes with the app's classes, which are loaded by the same classloader (the system +// classloader). +def agentRepackaged = "${agentPackage}.deps" + +dependencies { + compileOnly libraries.auto_service + compileOnly libraries.grpc_context + compileOnly project(':opencensus-api') + compile libraries.byte_buddy + compile libraries.config + compile libraries.findbugs_annotations + compile libraries.guava + + signature 'org.codehaus.mojo.signature:java17:1.0@signature' +} + +jar { + manifest { + // Set the required manifest attributes for the Java agent, cf. + // https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html. + attributes 'Premain-Class': agentMainClass + attributes 'Can-Retransform-Classes': true + } +} + +// Create bootstrap.jar containing the classes that need to be loaded by the bootstrap +// classloader. +task bootstrapJar(type: Jar) { + // Output to 'bootstrap.jar'. + baseName = 'bootstrap' + version = null + + from sourceSets.main.output + include agentBootstrapClasses +} + +shadowJar.dependsOn bootstrapJar + +// Bundle the agent's classes and dependencies into a single, self-contained JAR file. +shadowJar { + // Output to opencensus-contrib-agent-VERSION.jar. + classifier = null + + // Include only the following dependencies (excluding transitive dependencies). + dependencies { + include(dependency(libraries.byte_buddy)) + include(dependency(libraries.config)) + include(dependency(libraries.guava)) + } + + // Exclude cruft which still snuck in. + exclude 'META-INF/maven/**' + exclude agentBootstrapClasses + + // Relocate third party packages to avoid any conflicts of the agent's classes with the app's + // classes, which are loaded by the same classloader (the system classloader). + // Byte Buddy: + relocate 'net.bytebuddy', agentRepackaged + '.bytebuddy' + // Config: + relocate 'com.typesafe.config', agentRepackaged + '.config' + // Guava: + relocate 'com.google.common', agentRepackaged + '.guava' + relocate 'com.google.thirdparty.publicsuffix', agentRepackaged + '.publicsuffix' + + doLast { + def agentPackageDir = agentPackage.replace('.', '/') + '/' + def agentBootstrapJar = agentPackageDir + 'bootstrap.jar' + + // Bundle bootstrap.jar. + ant.jar(update: 'true', destfile: shadowJar.archivePath) { + mappedresources { + fileset(file: bootstrapJar.archivePath) + globmapper(from: '*', to: agentBootstrapJar) + } + } + + // Assert that there's nothing obviously wrong with the JAR's contents. + new java.util.zip.ZipFile(shadowJar.archivePath).withCloseable { + // Must have bundled the bootstrap.jar. + assert it.entries().any { it.name == agentBootstrapJar } + + it.entries().each { entry -> + // Must not contain anything outside of ${agentPackage}, ... + assert entry.name.startsWith(agentPackageDir) || + // ... except for the expected entries. + [ agentPackageDir, + 'META-INF/MANIFEST.MF', + 'META-INF/services/io.opencensus.contrib.agent.instrumentation.Instrumenter', + 'reference.conf', + ].any { entry.isDirectory() ? it.startsWith(entry.name) : it == entry.name } + // Also, should not have the bootstrap classes. + assert !entry.name.startsWith(agentBootstrapPackageDir) + } + } + } +} + +jar.finalizedBy shadowJar + +// TODO(stschmidt): Proguard-shrink the agent JAR. + +// Integration tests. The setup was initially based on +// https://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-integration-testing/. +// We run the same suite of integration tests on different Java versions with the agent enabled. +// The JAVA_HOMES environment variable lists the home directories of the Java installations used +// for integration testing. + +// The default JAR has been replaced with a self-contained JAR by the shadowJar task. Therefore, +// remove all declared dependencies from the generated Maven POM for said JAR. +uploadArchives { + repositories { + mavenDeployer { + pom.whenConfigured { + dependencies = [] + } + } + } +} + +sourceSets { + integrationTest { + java { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/java') + } + resources.srcDir file('src/integration-test/resources') + } +} + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +dependencies { + integrationTestCompile project(':opencensus-api') + integrationTestCompile project(':opencensus-testing') + integrationTestRuntime libraries.grpc_context + integrationTestRuntime project(':opencensus-impl-lite') +} + +// Disable checkstyle for integration tests if not java8. +checkstyleIntegrationTest.enabled = JavaVersion.current().isJava8Compatible() + +// Disable findbugs for integration tests, too. +findbugsIntegrationTest.enabled = false + +def javaExecutables = (System.getenv('JAVA_HOMES') ?: '') + .tokenize(File.pathSeparator) + .plus(System.getProperty('java.home')) + .collect { org.apache.tools.ant.taskdefs.condition.Os.isFamily( + org.apache.tools.ant.taskdefs.condition.Os.FAMILY_WINDOWS) + ? "${it}/bin/java.exe" + : "${it}/bin/java" } + .collect { new File(it).getCanonicalPath() } + .unique() + +assert javaExecutables.size > 0 : + 'No Java executables found for running integration tests' + +task integrationTest + +javaExecutables.eachWithIndex { javaExecutable, index -> + def perVersionIntegrationTest = task("integrationTest_${index}", type: Test) { + testLogging { + // Let Gradle output the stdout and stderr from tests, too. This is useful for investigating + // test failures on Travis, where we can't view Gradle's test reports. + showStandardStreams = true + + // Include the exception message and full stacktrace for failed tests. + exceptionFormat 'full' + } + + dependsOn shadowJar + + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + + executable = javaExecutable + + // The JaCoCo agent must be specified first so that it can instrument our agent. + // This is a work around for the issue that the JaCoCo agent is added last, cf. + // https://discuss.gradle.org/t/jacoco-gradle-adds-the-agent-last-to-jvm-args/7124. + doFirst { + jvmArgs jacoco.asJvmArg // JaCoCo agent first. + jvmArgs "-javaagent:${shadowJar.archivePath}" // Our agent second. + jacoco.enabled = false // Don't add the JaCoCo agent again. + } + + doFirst { logger.lifecycle("Running integration tests using ${javaExecutable}.") } + } + + integrationTest.dependsOn perVersionIntegrationTest +} + +check.dependsOn integrationTest +integrationTest.mustRunAfter test + +// Merge JaCoCo's execution data from all tests into the main test's execution data file. +task jacocoMerge(type: JacocoMerge) { + tasks.withType(Test).each { testTask -> + dependsOn testTask + executionData testTask.jacoco.destinationFile + } + doLast { + destinationFile.renameTo test.jacoco.destinationFile + } +} + +jacocoTestReport.dependsOn jacocoMerge + +// JMH benchmarks + +dependencies { + jmh libraries.grpc_context +} + +// Make the agent JAR available using a fixed file name so that we don't have to modify the JMH +// benchmarks whenever the version changes. +task agentJar(type: Copy) { + dependsOn shadowJar + + from shadowJar.archivePath + into libsDir + rename { 'agent.jar' } +} + +jmhJar.dependsOn agentJar +jmhJar.dependsOn integrationTest diff --git a/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java new file mode 100644 index 00000000..7cab5590 --- /dev/null +++ b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java @@ -0,0 +1,195 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.Context; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for {@link ExecutorInstrumentation}. + * + * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via + * the {@code -javaagent} command line option. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class ExecutorInstrumentationIT { + + private static final Context.Key<String> KEY = Context.key("mykey"); + + private ExecutorService executor; + private Context previousContext; + + @Before + public void beforeMethod() { + executor = Executors.newCachedThreadPool(); + } + + @After + public void afterMethod() { + Context.current().detach(previousContext); + executor.shutdown(); + } + + @Test(timeout = 60000) + public void execute() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final Semaphore tested = new Semaphore(0); + + executor.execute( + new Runnable() { + @Override + public void run() { + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.release(); + } + }); + + tested.acquire(); + } + + @Test(timeout = 60000) + public void submit_Callable() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + + executor + .submit( + new Callable<Void>() { + @Override + public Void call() throws Exception { + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + + return null; + } + }) + .get(); + + assertThat(tested.get()).isTrue(); + } + + @Test(timeout = 60000) + public void submit_Runnable() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + + executor + .submit( + new Runnable() { + @Override + public void run() { + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + } + }) + .get(); + + assertThat(tested.get()).isTrue(); + } + + @Test(timeout = 60000) + public void submit_RunnableWithResult() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + Object result = new Object(); + + Future<Object> future = + executor.submit( + new Runnable() { + @Override + public void run() { + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isNotSameAs(Context.ROOT); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + } + }, + result); + + assertThat(future.get()).isSameAs(result); + assertThat(tested.get()).isTrue(); + } + + @Test(timeout = 60000) + public void currentContextExecutor() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final Semaphore tested = new Semaphore(0); + + Context.currentContextExecutor(executor) + .execute( + new Runnable() { + @Override + public void run() { + StackTraceElement[] ste = new Exception().fillInStackTrace().getStackTrace(); + assertThat(ste[0].getClassName()).doesNotContain("Context"); + assertThat(ste[1].getClassName()).startsWith("io.grpc.Context$"); + // NB: Actually, we want the Runnable to be wrapped only once, but currently it is + // still wrapped twice. The two places where the Runnable is wrapped are: (1) the + // executor implementation itself, e.g. ThreadPoolExecutor, to which the Agent added + // automatic context propagation, (2) CurrentContextExecutor. + // ExecutorInstrumentation already avoids adding the automatic context propagation + // to CurrentContextExecutor, but does not make it a no-op yet. Also see + // ExecutorInstrumentation#createMatcher. + assertThat(ste[2].getClassName()).startsWith("io.grpc.Context$"); + assertThat(ste[3].getClassName()).doesNotContain("Context"); + + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + + tested.release(); + } + }); + + tested.acquire(); + } +} diff --git a/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java new file mode 100644 index 00000000..f718f492 --- /dev/null +++ b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java @@ -0,0 +1,144 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.Context; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for {@link ThreadInstrumentation}. + * + * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via + * the {@code -javaagent} command line option. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class ThreadInstrumentationIT { + + private static final Context.Key<String> KEY = Context.key("mykey"); + + private Context previousContext; + + @After + public void afterMethod() { + Context.current().detach(previousContext); + } + + @Test(timeout = 60000) + public void start_Runnable() throws Exception { + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + + Runnable runnable = + new Runnable() { + @Override + public void run() { + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + } + }; + Thread thread = new Thread(runnable); + + thread.start(); + thread.join(); + + assertThat(tested.get()).isTrue(); + } + + @Test(timeout = 60000) + public void start_Subclass() throws Exception { + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + + class MyThread extends Thread { + + @Override + public void run() { + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + } + } + + Thread thread = new MyThread(); + + thread.start(); + thread.join(); + + assertThat(tested.get()).isTrue(); + } + + /** + * Tests that the automatic context propagation added by {@link ThreadInstrumentation} does not + * interfere with the automatically propagated context from Executor#execute. + */ + @Test(timeout = 60000) + public void start_automaticallyWrappedRunnable() throws Exception { + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + Executor newThreadExecutor = + new Executor() { + @Override + public void execute(Runnable command) { + // Attach a new context before starting a new thread. This new context will be + // propagated to the new thread as in #start_Runnable. However, since the Runnable has + // been wrapped in a different context (by automatic instrumentation of + // Executor#execute), that context will be attached when executing the Runnable. + Context context2 = Context.current().withValue(KEY, "wrong context"); + Context context3 = context2.attach(); + try { + Thread thread = new Thread(command); + thread.start(); + try { + thread.join(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } finally { + context2.detach(context3); + } + } + }; + + final AtomicReference<Context> newThreadCtx = new AtomicReference<Context>(); + newThreadExecutor.execute( + new Runnable() { + @Override + public void run() { + newThreadCtx.set(Context.current()); + } + }); + + // Assert that the automatic context propagation added by ThreadInstrumentation did not + // interfere with the automatically propagated context from Executor#execute. + assertThat(newThreadCtx.get()).isSameAs(context); + } +} diff --git a/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java new file mode 100644 index 00000000..163f3cd8 --- /dev/null +++ b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import io.opencensus.testing.export.TestHandler; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.export.SpanData; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for {@link UrlInstrumentation}. + * + * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via + * the {@code -javaagent} command line option. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class UrlInstrumentationIT { + + private static final TestHandler testHandler = new TestHandler(); + + @BeforeClass + public static void beforeClass() { + Tracing.getExportComponent().getSpanExporter().registerHandler("test", testHandler); + } + + @AfterClass + public static void afterClass() { + Tracing.getExportComponent().getSpanExporter().unregisterHandler("test"); + } + + @Test(timeout = 60000) + public void getContent() throws Exception { + URL url = getClass().getResource("some_resource.txt").toURI().toURL(); + Object content = url.getContent(); + + assertThat(content).isInstanceOf(InputStream.class); + assertThat(CharStreams.toString(new InputStreamReader((InputStream) content, Charsets.UTF_8))) + .isEqualTo("Some resource."); + + SpanData span = testHandler.waitForExport(1).get(0); + assertThat(span.getName()).isEqualTo("java.net.URL#getContent"); + assertThat(span.getStatus().isOk()).isTrue(); + } + + @Test(timeout = 60000) + public void getContent_fails() throws MalformedURLException { + URL url = new URL("file:///nonexistent"); + + try { + url.getContent(); + fail(); + } catch (IOException e) { + SpanData span = testHandler.waitForExport(1).get(0); + assertThat(span.getName()).isEqualTo("java.net.URL#getContent"); + assertThat(span.getStatus().isOk()).isFalse(); + } + } +} diff --git a/contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt b/contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt new file mode 100644 index 00000000..7e8787cb --- /dev/null +++ b/contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt @@ -0,0 +1 @@ +Some resource.
\ No newline at end of file diff --git a/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java new file mode 100644 index 00000000..7c2d4423 --- /dev/null +++ b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java @@ -0,0 +1,84 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.Context; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.infra.Blackhole; + +/** Benchmarks for automatic context propagation added by {@link ExecutorInstrumentation}. */ +public class ExecutorInstrumentationBenchmark { + + private static final class MyRunnable implements Runnable { + + private final Blackhole blackhole; + + private MyRunnable(Blackhole blackhole) { + this.blackhole = blackhole; + } + + @Override + public void run() { + blackhole.consume(Context.current()); + } + } + + /** + * This benchmark attempts to measure the performance without any context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Fork + public void none(final Blackhole blackhole) { + MoreExecutors.directExecutor().execute(new MyRunnable(blackhole)); + } + + /** + * This benchmark attempts to measure the performance with manual context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Fork + public void manual(final Blackhole blackhole) { + MoreExecutors.directExecutor().execute(Context.current().wrap(new MyRunnable(blackhole))); + } + + /** + * This benchmark attempts to measure the performance with automatic context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Fork(jvmArgsAppend = "-javaagent:contrib/agent/build/libs/agent.jar") + public void automatic(final Blackhole blackhole) { + MoreExecutors.directExecutor().execute(new MyRunnable(blackhole)); + } +} diff --git a/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java new file mode 100644 index 00000000..706c6d3a --- /dev/null +++ b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java @@ -0,0 +1,89 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import io.grpc.Context; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.infra.Blackhole; + +/** Naive benchmarks for automatic context propagation added by {@link ThreadInstrumentation}. */ +public class ThreadInstrumentationBenchmark { + + private static final class MyRunnable implements Runnable { + + private final Blackhole blackhole; + + private MyRunnable(Blackhole blackhole) { + this.blackhole = blackhole; + } + + @Override + public void run() { + blackhole.consume(Context.current()); + } + } + + /** + * This benchmark attempts to measure the performance without any context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Fork + public void none(Blackhole blackhole) throws InterruptedException { + Thread t = new Thread(new MyRunnable(blackhole)); + t.start(); + t.join(); + } + + /** + * This benchmark attempts to measure the performance with manual context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Fork + public void manual(Blackhole blackhole) throws InterruptedException { + Thread t = new Thread((Context.current().wrap(new MyRunnable(blackhole)))); + t.start(); + t.join(); + } + + /** + * This benchmark attempts to measure the performance with automatic context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Fork(jvmArgsAppend = "-javaagent:contrib/agent/build/libs/agent.jar") + public void automatic(Blackhole blackhole) throws InterruptedException { + Thread t = new Thread(new MyRunnable(blackhole)); + t.start(); + t.join(); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java new file mode 100644 index 00000000..54a82442 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent; + +import java.util.logging.Level; +import java.util.logging.Logger; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; + +/** + * An {@link AgentBuilder.Listener} which uses {@link java.util.logging} for logging events of + * interest. + */ +final class AgentBuilderListener implements AgentBuilder.Listener { + + private static final Logger logger = Logger.getLogger(AgentBuilderListener.class.getName()); + + @Override + public void onTransformation( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + DynamicType dynamicType) { + logger.log(Level.FINE, "{0}", typeDescription); + } + + @Override + public void onIgnored( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + boolean loaded) {} + + @Override + public void onError( + String typeName, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + Throwable throwable) { + logger.log(Level.WARNING, "Failed to handle " + typeName, throwable); + } + + @Override + public void onComplete( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {} + + @Override + public void onDiscovery( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {} +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java new file mode 100644 index 00000000..49c568ed --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import io.opencensus.contrib.agent.bootstrap.ContextStrategy; +import io.opencensus.contrib.agent.bootstrap.ContextTrampoline; +import io.opencensus.contrib.agent.instrumentation.Instrumenter; +import java.lang.instrument.Instrumentation; +import java.util.ServiceLoader; +import java.util.jar.JarFile; +import java.util.logging.Logger; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * The <b>OpenCensus Agent for Java</b> collects and sends latency data about your Java process to + * OpenCensus backends such as Stackdriver Trace for analysis and visualization. + * + * <p>To enable the *OpenCensus Agent for Java* for your application, add the option {@code + * -javaagent:path/to/opencensus-contrib-agent.jar} to the invocation of the {@code java} executable + * as shown in the following example: + * + * <pre> + * java -javaagent:path/to/opencensus-contrib-agent.jar ... + * </pre> + * + * @see <a + * href="https://github.com/census-instrumentation/instrumentation-java/tree/master/agent">https://github.com/census-instrumentation/instrumentation-java/tree/master/agent</a> + * @since 0.6 + */ +public final class AgentMain { + + private static final Logger logger = Logger.getLogger(AgentMain.class.getName()); + + private AgentMain() {} + + /** + * Initializes the OpenCensus Agent for Java. + * + * @param agentArgs agent options, passed as a single string by the JVM + * @param instrumentation the {@link Instrumentation} object provided by the JVM for instrumenting + * Java programming language code + * @throws Exception if initialization of the agent fails + * @see java.lang.instrument + * @since 0.6 + */ + public static void premain(String agentArgs, Instrumentation instrumentation) throws Exception { + checkNotNull(instrumentation, "instrumentation"); + + logger.fine("Initializing."); + + // The classes in bootstrap.jar, such as ContextManger and ContextStrategy, will be referenced + // from classes loaded by the bootstrap classloader. Thus, these classes have to be loaded by + // the bootstrap classloader, too. + instrumentation.appendToBootstrapClassLoaderSearch( + new JarFile(Resources.getResourceAsTempFile("bootstrap.jar"))); + + checkLoadedByBootstrapClassloader(ContextTrampoline.class); + checkLoadedByBootstrapClassloader(ContextStrategy.class); + + Settings settings = Settings.load(); + AgentBuilder agentBuilder = + new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .with(new AgentBuilderListener()) + .ignore(none()); + for (Instrumenter instrumenter : ServiceLoader.load(Instrumenter.class)) { + agentBuilder = instrumenter.instrument(agentBuilder, settings); + } + agentBuilder.installOn(instrumentation); + + logger.fine("Initialized."); + } + + private static void checkLoadedByBootstrapClassloader(Class<?> clazz) { + checkState( + clazz.getClassLoader() == null, "%s must be loaded by the bootstrap classloader", clazz); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java new file mode 100644 index 00000000..7367b85a --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.io.ByteStreams; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** Helper methods for working with resources. */ +final class Resources { + private Resources() {} + + /** + * Returns a resource of the given name as a temporary file. + * + * @param resourceName name of the resource + * @return a temporary {@link File} containing a copy of the resource + * @throws FileNotFoundException if no resource of the given name is found + * @throws IOException if an I/O error occurs + */ + static File getResourceAsTempFile(String resourceName) throws IOException { + checkArgument(!Strings.isNullOrEmpty(resourceName), "resourceName"); + + File file = File.createTempFile(resourceName, ".tmp"); + OutputStream os = new FileOutputStream(file); + try { + getResourceAsTempFile(resourceName, file, os); + return file; + } finally { + os.close(); + } + } + + @VisibleForTesting + static void getResourceAsTempFile(String resourceName, File file, OutputStream outputStream) + throws IOException { + file.deleteOnExit(); + + InputStream is = getResourceAsStream(resourceName); + try { + ByteStreams.copy(is, outputStream); + } finally { + is.close(); + } + } + + private static InputStream getResourceAsStream(String resourceName) throws FileNotFoundException { + InputStream is = Resources.class.getResourceAsStream(resourceName); + if (is == null) { + throw new FileNotFoundException( + "Cannot find resource '" + resourceName + "' on the class path."); + } + return is; + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java new file mode 100644 index 00000000..46fe395d --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import io.opencensus.common.Internal; + +/** + * The {@code Settings} class provides access to user-configurable settings. + * + * @since 0.10 + */ +public class Settings { + + private static final String CONFIG_ROOT = "opencensus.contrib.agent"; + + private final Config config; + + /** Creates agent settings. */ + @Internal + @VisibleForTesting + public Settings(Config config) { + this.config = checkNotNull(config); + } + + static Settings load() { + return new Settings(readConfig()); + } + + private static Config readConfig() { + Config config = ConfigFactory.load(); + config.checkValid(ConfigFactory.defaultReference(), CONFIG_ROOT); + + return config.getConfig(CONFIG_ROOT); + } + + /** + * Checks whether a feature is enabled in the effective configuration. + * + * <p>A feature is identified by a path expression relative to {@link #CONFIG_ROOT}, such as + * {@code context-propagation.executor}. The feature is enabled iff the config element at the + * requested path has a child element {@code enabled} with a value of {@code true}, {@code on}, or + * {@code yes}. + * + * @param featurePath the feature's path expression + * @return true, if enabled, otherwise false + * @since 0.10 + */ + public boolean isEnabled(String featurePath) { + checkArgument(!Strings.isNullOrEmpty(featurePath)); + + return config.getConfig(featurePath).getBoolean("enabled"); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java new file mode 100644 index 00000000..57d4efca --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.bootstrap; + +/** + * Strategy interface for accessing and manipulating the context. + * + * @since 0.6 + */ +public interface ContextStrategy { + + /** + * Wraps a {@link Runnable} so that it executes with the context that is associated with the + * current scope. + * + * @param runnable a {@link Runnable} object + * @return the wrapped {@link Runnable} object + * @since 0.6 + */ + Runnable wrapInCurrentContext(Runnable runnable); + + /** + * Saves the context that is associated with the current scope. + * + * <p>The context will be attached when entering the specified thread's {@link Thread#run()} + * method. + * + * @param thread a {@link Thread} object + * @since 0.6 + */ + void saveContextForThread(Thread thread); + + /** + * Attaches the context that was previously saved for the specified thread. + * + * @param thread a {@link Thread} object + * @since 0.6 + */ + void attachContextForThread(Thread thread); +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java new file mode 100644 index 00000000..2e737be2 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.bootstrap; + +/** + * {@code ContextTrampoline} provides methods for accessing and manipulating the context from + * instrumented bytecode. + * + * <p>{@code ContextTrampoline} avoids tight coupling with the concrete implementation of the + * context by accessing and manipulating the context through the {@link ContextStrategy} interface. + * + * <p>Both {@link ContextTrampoline} and {@link ContextStrategy} are loaded by the bootstrap + * classloader so that they can be used from classes loaded by the bootstrap classloader. A concrete + * implementation of {@link ContextStrategy} will be loaded by the system classloader. This allows + * for using the same context implementation as the instrumented application. + * + * <p>{@code ContextTrampoline} is implemented as a static class to allow for easy and fast use from + * instrumented bytecode. We cannot use dependency injection for the instrumented bytecode. + * + * @since 0.9 + */ +// TODO(sebright): Fix the Checker Framework warnings. +@SuppressWarnings("nullness") +public final class ContextTrampoline { + + // Not synchronized to avoid any synchronization costs after initialization. + // The agent is responsible for initializing this once (through #setContextStrategy) before any + // other method of this class is called. + private static ContextStrategy contextStrategy; + + private ContextTrampoline() {} + + /** + * Sets the concrete strategy for accessing and manipulating the context. + * + * <p>NB: The agent is responsible for setting the context strategy once before any other method + * of this class is called. + * + * @param contextStrategy the concrete strategy for accessing and manipulating the context + * @since 0.9 + */ + public static void setContextStrategy(ContextStrategy contextStrategy) { + if (ContextTrampoline.contextStrategy != null) { + throw new IllegalStateException("contextStrategy was already set"); + } + + if (contextStrategy == null) { + throw new NullPointerException("contextStrategy"); + } + + ContextTrampoline.contextStrategy = contextStrategy; + } + + /** + * Wraps a {@link Runnable} so that it executes with the context that is associated with the + * current scope. + * + * @param runnable a {@link Runnable} object + * @return the wrapped {@link Runnable} object + * @see ContextStrategy#wrapInCurrentContext + * @since 0.9 + */ + public static Runnable wrapInCurrentContext(Runnable runnable) { + return contextStrategy.wrapInCurrentContext(runnable); + } + + /** + * Saves the context that is associated with the current scope. + * + * <p>The context will be attached when entering the specified thread's {@link Thread#run()} + * method. + * + * @param thread a {@link Thread} object + * @since 0.9 + */ + public static void saveContextForThread(Thread thread) { + contextStrategy.saveContextForThread(thread); + } + + /** + * Attaches the context that was previously saved for the specified thread. + * + * @param thread a {@link Thread} object + * @since 0.9 + */ + public static void attachContextForThread(Thread thread) { + contextStrategy.attachContextForThread(thread); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java new file mode 100644 index 00000000..363dbbdc --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.bootstrap; + +import com.google.errorprone.annotations.MustBeClosed; +import java.io.Closeable; +import javax.annotation.Nullable; + +/** + * Strategy interface for creating and manipulating trace spans. + * + * @since 0.9 + */ +public interface TraceStrategy { + + /** + * Starts a new span and sets it as the current span. + * + * <p>Enters the scope of code where the newly created {@code Span} is in the current Context, and + * returns an object that represents that scope. When the returned object is closed, the scope is + * exited, the previous Context is restored, and the newly created {@code Span} is ended using + * {@link io.opencensus.trace.Span#end}. + * + * <p>Callers must eventually close the returned object to avoid leaking the Context. + * + * <p>Supports the try-with-resource idiom. + * + * <p>NB: The return type of this method is intentionally {@link Closeable} and not the more + * specific {@link io.opencensus.common.Scope} because the latter would not be visible from + * classes loaded by the bootstrap classloader. + * + * @param spanName the name of the returned {@link io.opencensus.trace.Span} + * @return an object that defines a scope where the newly created {@code Span} will be set to the + * current Context + * @see io.opencensus.trace.Tracer#spanBuilder(java.lang.String) + * @see io.opencensus.trace.SpanBuilder#startScopedSpan() + * @since 0.9 + */ + @MustBeClosed + Closeable startScopedSpan(String spanName); + + /** + * Ends the current span with a status derived from the given (optional) Throwable, and closes the + * given scope. + * + * @param scope an object representing the scope + * @param throwable an optional Throwable + * @since 0.9 + */ + void endScope(Closeable scope, @Nullable Throwable throwable); +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java new file mode 100644 index 00000000..aeae2592 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.bootstrap; + +import com.google.errorprone.annotations.MustBeClosed; +import java.io.Closeable; +import javax.annotation.Nullable; + +/** + * {@code TraceTrampoline} provides methods for creating and manipulating trace spans from + * instrumented bytecode. + * + * <p>{@code TraceTrampoline} avoids tight coupling with the concrete trace API through the {@link + * TraceStrategy} interface. + * + * <p>Both {@link TraceTrampoline} and {@link TraceStrategy} are loaded by the bootstrap classloader + * so that they can be used from classes loaded by the bootstrap classloader. A concrete + * implementation of {@link TraceStrategy} will be loaded by the system classloader. This allows for + * using the same trace API as the instrumented application. + * + * <p>{@code TraceTrampoline} is implemented as a static class to allow for easy and fast use from + * instrumented bytecode. We cannot use dependency injection for the instrumented bytecode. + * + * @since 0.9 + */ +// TODO(sebright): Fix the Checker Framework warnings. +@SuppressWarnings("nullness") +public final class TraceTrampoline { + + // Not synchronized to avoid any synchronization costs after initialization. + // The agent is responsible for initializing this once (through #setTraceStrategy) before any + // other method of this class is called. + private static TraceStrategy traceStrategy; + + private TraceTrampoline() {} + + /** + * Sets the concrete strategy for creating and manipulating trace spans. + * + * <p>NB: The agent is responsible for setting the trace strategy once before any other method of + * this class is called. + * + * @param traceStrategy the concrete strategy for creating and manipulating trace spans + * @since 0.9 + */ + public static void setTraceStrategy(TraceStrategy traceStrategy) { + if (TraceTrampoline.traceStrategy != null) { + throw new IllegalStateException("traceStrategy was already set"); + } + + if (traceStrategy == null) { + throw new NullPointerException("traceStrategy"); + } + + TraceTrampoline.traceStrategy = traceStrategy; + } + + /** + * Starts a new span and sets it as the current span. + * + * <p>Enters the scope of code where the newly created {@code Span} is in the current Context, and + * returns an object that represents that scope. When the returned object is closed, the scope is + * exited, the previous Context is restored, and the newly created {@code Span} is ended using + * {@link io.opencensus.trace.Span#end}. + * + * <p>Callers must eventually close the returned object to avoid leaking the Context. + * + * <p>Supports the try-with-resource idiom. + * + * <p>NB: The return type of this method is intentionally {@link Closeable} and not the more + * specific {@link io.opencensus.common.Scope} because the latter would not be visible from + * classes loaded by the bootstrap classloader. + * + * @param spanName the name of the returned {@link io.opencensus.trace.Span} + * @return an object that defines a scope where the newly created {@code Span} will be set to the + * current Context + * @see io.opencensus.trace.Tracer#spanBuilder(String) + * @see io.opencensus.trace.SpanBuilder#startScopedSpan() + * @since 0.9 + */ + @MustBeClosed + public static Closeable startScopedSpan(String spanName) { + return traceStrategy.startScopedSpan(spanName); + } + + /** + * Ends the current span with a status derived from the given (optional) Throwable, and closes the + * given scope. + * + * @param scope an object representing the scope + * @param throwable an optional Throwable + * @since 0.9 + */ + public static void endScope(Closeable scope, @Nullable Throwable throwable) { + traceStrategy.endScope(scope, throwable); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java new file mode 100644 index 00000000..f1363a26 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.bootstrap; + +/** + * Contains classes that need to be loaded by the bootstrap classloader because they are used from + * classes loaded by the bootstrap classloader. + * + * <p>NB: Do not add direct dependencies on classes that are not loaded by the bootstrap + * classloader. Keep this package small. + */ diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java new file mode 100644 index 00000000..71e81270 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.deps; + +/** + * Contains third party packages, such as Byte Buddy, Guava, etc., relocated here by the build + * process to avoid any conflicts of the agent's classes with the app's classes, which are loaded by + * the same classloader (the system classloader). + */ diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java new file mode 100644 index 00000000..8a6d8a6c --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import io.grpc.Context; +import io.opencensus.contrib.agent.bootstrap.ContextStrategy; +import java.lang.ref.WeakReference; + +/** + * Implementation of {@link ContextStrategy} for accessing and manipulating the {@link + * io.grpc.Context}. + */ +final class ContextStrategyImpl implements ContextStrategy { + + /** + * Thread-safe mapping of {@link Thread}s to {@link Context}s, used for tunneling the caller's + * {@link Context} of {@link Thread#start()} to {@link Thread#run()}. + * + * <p>A thread is inserted into this map when {@link Thread#start()} is called, and removed when + * {@link Thread#run()} is called. + * + * <p>NB: {@link Thread#run()} is not guaranteed to be called after {@link Thread#start()}, for + * example when attempting to start a thread a second time. Therefore, threads are wrapped in + * {@link WeakReference}s so that this map does not prevent the garbage collection of otherwise + * unreferenced threads. Unreferenced threads will be automatically removed from the map by the + * routine cleanup of the underlying {@link Cache} implementation. + * + * <p>NB: A side-effect of {@link CacheBuilder#weakKeys()} is the use of identity ({@code ==}) + * comparison to determine equality of threads. Identity comparison is required here because + * subclasses of {@link Thread} might override {@link Object#hashCode()} and {@link + * Object#equals(java.lang.Object)} with potentially broken implementations. + * + * <p>NB: Using thread IDs as keys was considered: It's unclear how to safely detect and cleanup + * otherwise unreferenced threads IDs from the map. + */ + private final Cache<Thread, Context> savedContexts = CacheBuilder.newBuilder().weakKeys().build(); + + @Override + public Runnable wrapInCurrentContext(Runnable runnable) { + return Context.current().wrap(runnable); + } + + @Override + public void saveContextForThread(Thread thread) { + savedContexts.put(thread, Context.current()); + } + + @Override + public void attachContextForThread(Thread thread) { + if (Thread.currentThread() == thread) { + Context context = savedContexts.getIfPresent(thread); + if (context != null) { + savedContexts.invalidate(thread); + // Work around findbugs warning. Context.attach() is marked as @CheckReturnValue so we need + // to check the return + // value here, otherwise findbugs will fail. + Preconditions.checkNotNull(context.attach(), "context.attach()"); + } + } + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java new file mode 100644 index 00000000..17a5b1d9 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import com.google.auto.service.AutoService; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.ContextStrategy; +import io.opencensus.contrib.agent.bootstrap.ContextTrampoline; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * Initializes the {@link ContextTrampoline} with a concrete {@link ContextStrategy}. + * + * @since 0.9 + */ +@AutoService(Instrumenter.class) +public final class ContextTrampolineInitializer implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + // TODO(stschmidt): Gracefully handle the case of missing io.grpc.Context at runtime, + // maybe load the missing classes from a JAR that comes with the agent JAR. + ContextTrampoline.setContextStrategy(new ContextStrategyImpl()); + + return agentBuilder; + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java new file mode 100644 index 00000000..1e1429ce --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.base.Preconditions.checkNotNull; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; +import static net.bytebuddy.matcher.ElementMatchers.nameEndsWith; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.ContextTrampoline; +import java.util.concurrent.Executor; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.utility.JavaModule; + +/** + * Propagates the context of the caller of {@link Executor#execute} to the submitted {@link + * Runnable}, just like the Microsoft .Net Framework propagates the <a + * href="https://msdn.microsoft.com/en-us/library/system.threading.executioncontext(v=vs.110).aspx">System.Threading.ExecutionContext</a>. + * + * @since 0.6 + */ +@AutoService(Instrumenter.class) +public final class ExecutorInstrumentation implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + checkNotNull(agentBuilder, "agentBuilder"); + checkNotNull(settings, "settings"); + + if (!settings.isEnabled("context-propagation.executor")) { + return agentBuilder; + } + + return agentBuilder.type(createMatcher()).transform(new Transformer()); + } + + private static class Transformer implements AgentBuilder.Transformer { + + @Override + public DynamicType.Builder<?> transform( + DynamicType.Builder<?> builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + return builder.visit(Advice.to(Execute.class).on(named("execute"))); + } + } + + private static ElementMatcher.Junction<TypeDescription> createMatcher() { + // This matcher matches implementations of Executor, but excludes CurrentContextExecutor and + // FixedContextExecutor from io.grpc.Context, which already propagate the context. + // TODO(stschmidt): As the executor implementation itself (e.g. ThreadPoolExecutor) is + // instrumented by the agent for automatic context propagation, CurrentContextExecutor could be + // turned into a no-op to avoid another unneeded context propagation. Likewise, when using + // FixedContextExecutor, the automatic context propagation added by the agent is unneeded. + return isSubTypeOf(Executor.class) + .and(not(isAbstract())) + .and( + not( + nameStartsWith("io.grpc.Context$") + .and( + nameEndsWith("CurrentContextExecutor") + .or(nameEndsWith("FixedContextExecutor"))))); + } + + private static class Execute { + + /** + * Wraps a {@link Runnable} so that it executes with the context that is associated with the + * current scope. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Executor#execute. + * + * @see Advice + */ + @Advice.OnMethodEnter + @SuppressWarnings(value = "UnusedAssignment") + @SuppressFBWarnings(value = {"DLS_DEAD_LOCAL_STORE", "UPM_UNCALLED_PRIVATE_METHOD"}) + private static void enter(@Advice.Argument(value = 0, readOnly = false) Runnable runnable) { + runnable = ContextTrampoline.wrapInCurrentContext(runnable); + } + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java new file mode 100644 index 00000000..5eb197ee --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import io.opencensus.contrib.agent.Settings; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * Interface for plug-ins that add bytecode instrumentation. + * + * @since 0.6 + */ +public interface Instrumenter { + + /** + * Adds bytecode instrumentation to the given {@link AgentBuilder}. + * + * @param agentBuilder an {@link AgentBuilder} object to which the additional instrumentation is + * added + * @param settings the configuration settings + * @return an {@link AgentBuilder} object having the additional instrumentation + * @since 0.10 + */ + AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings); +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java new file mode 100644 index 00000000..b4beba8e --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.base.Preconditions.checkNotNull; +import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.ContextTrampoline; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; + +/** + * Propagates the context of the caller of {@link Thread#start} to the new thread, just like the + * Microsoft .Net Framework propagates the <a + * href="https://msdn.microsoft.com/en-us/library/system.threading.executioncontext(v=vs.110).aspx">System.Threading.ExecutionContext</a>. + * + * <p>NB: A similar effect could be achieved with {@link InheritableThreadLocal}, but the semantics + * are different: {@link InheritableThreadLocal} inherits values when the thread object is + * initialized as opposed to when {@link Thread#start()} is called. + * + * @since 0.6 + */ +@AutoService(Instrumenter.class) +public final class ThreadInstrumentation implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + checkNotNull(agentBuilder, "agentBuilder"); + checkNotNull(settings, "settings"); + + if (!settings.isEnabled("context-propagation.thread")) { + return agentBuilder; + } + + return agentBuilder.type(isSubTypeOf(Thread.class)).transform(new Transformer()); + } + + private static class Transformer implements AgentBuilder.Transformer { + + @Override + public DynamicType.Builder<?> transform( + DynamicType.Builder<?> builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + return builder + .visit(Advice.to(Start.class).on(named("start"))) + .visit(Advice.to(Run.class).on(named("run"))); + } + } + + private static class Start { + + /** + * Saves the context that is associated with the current scope. + * + * <p>The context will be attached when entering the thread's {@link Thread#run()} method. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Thread#start. + * + * @see Advice + */ + @Advice.OnMethodEnter + @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") + private static void enter(@Advice.This Thread thread) { + ContextTrampoline.saveContextForThread(thread); + } + } + + private static class Run { + + /** + * Attaches the context that was previously saved for this thread. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Thread#run. + * + * @see Advice + */ + @Advice.OnMethodEnter + @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") + private static void enter(@Advice.This Thread thread) { + ContextTrampoline.attachContextForThread(thread); + } + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java new file mode 100644 index 00000000..139c10f3 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.errorprone.annotations.MustBeClosed; +import io.opencensus.contrib.agent.bootstrap.TraceStrategy; +import io.opencensus.trace.Status; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.samplers.Samplers; +import java.io.Closeable; +import java.io.IOException; +import javax.annotation.Nullable; + +/** Implementation of {@link TraceStrategy} for creating and manipulating trace spans. */ +final class TraceStrategyImpl implements TraceStrategy { + + @MustBeClosed + @Override + public Closeable startScopedSpan(String spanName) { + checkNotNull(spanName, "spanName"); + + return Tracing.getTracer() + .spanBuilder(spanName) + .setSampler(Samplers.alwaysSample()) + .setRecordEvents(true) + .startScopedSpan(); + } + + @Override + public void endScope(Closeable scope, @Nullable Throwable throwable) { + checkNotNull(scope, "scope"); + + if (throwable != null) { + Tracing.getTracer() + .getCurrentSpan() + .setStatus( + Status.UNKNOWN.withDescription( + throwable.getMessage() == null + ? throwable.getClass().getSimpleName() + : throwable.getMessage())); + } + + try { + scope.close(); + } catch (IOException ex) { + // Ignore. + } + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java new file mode 100644 index 00000000..4a68845c --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import com.google.auto.service.AutoService; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.TraceStrategy; +import io.opencensus.contrib.agent.bootstrap.TraceTrampoline; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * Initializes the {@link TraceTrampoline} with a concrete {@link TraceStrategy}. + * + * @since 0.9 + */ +@AutoService(Instrumenter.class) +public final class TraceTrampolineInitializer implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + // TODO(stschmidt): Gracefully handle the case of missing trace API at runtime, + // maybe load the missing classes from a JAR that comes with the agent JAR. + TraceTrampoline.setTraceStrategy(new TraceStrategyImpl()); + + return agentBuilder; + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java new file mode 100644 index 00000000..336f70b1 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.base.Preconditions.checkNotNull; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import com.google.errorprone.annotations.MustBeClosed; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.TraceTrampoline; +import java.io.Closeable; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; + +/** + * Wraps the execution of {@link java.net.URL#getContent()} in a trace span. + * + * <p>TODO(stschmidt): Replace this preliminary, java.net.URL-specific implementation with a + * generic, configurable implementation. + * + * @since 0.9 + */ +@AutoService(Instrumenter.class) +public final class UrlInstrumentation implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + checkNotNull(agentBuilder, "agentBuilder"); + checkNotNull(settings, "settings"); + + if (!settings.isEnabled("trace.java.net.URL.getContent")) { + return agentBuilder; + } + + return agentBuilder.type(named("java.net.URL")).transform(new Transformer()); + } + + private static class Transformer implements AgentBuilder.Transformer { + + @Override + public DynamicType.Builder<?> transform( + DynamicType.Builder<?> builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + return builder.visit(Advice.to(GetContent.class).on(named("getContent"))); + } + } + + private static class GetContent { + + /** + * Starts a new span and sets it as the current span when entering the method. + * + * <p>The name of the new span is constructed from the name of the instrumented class and + * method. For example, in case of {@link java.net.URL#getContent()} the span name is {@code + * java.net.URL#getContent}. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Executor#execute. + * + * @see Advice + */ + @Advice.OnMethodEnter + @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") + @MustBeClosed + private static Closeable enter(@Advice.Origin("#t\\##m") String classAndMethodName) { + return TraceTrampoline.startScopedSpan(classAndMethodName); + } + + /** + * Closes the current span and scope when exiting the method. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Executor#execute. + * + * <p>NB: By default, any {@link Throwable} thrown during the advice's execution is silently + * suppressed. + * + * @see Advice + */ + @Advice.OnMethodExit(onThrowable = Throwable.class) + @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") + private static void exit(@Advice.Enter Closeable scope, @Advice.Thrown Throwable throwable) { + TraceTrampoline.endScope(scope, throwable); + } + } +} diff --git a/contrib/agent/src/main/resources/reference.conf b/contrib/agent/src/main/resources/reference.conf new file mode 100644 index 00000000..e1781248 --- /dev/null +++ b/contrib/agent/src/main/resources/reference.conf @@ -0,0 +1,23 @@ +# Reference configuration for the OpenCensus Agent for Java. + +opencensus.contrib.agent { + + # Configuration settings related to automatic context propagation. + context-propagation { + + # Enable/disable automatic context propagation for Executors. + executor.enabled = true + + # Enable/disable automatic context propagation for Threads. + thread.enabled = true + } + + # The "trace" section configures which Java methods the agent instruments for + # tracing. + trace { + + java.net.URL.getContent { + enabled = true + } + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java new file mode 100644 index 00000000..26eb696b --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link Resources}. */ +@RunWith(MockitoJUnitRunner.class) +public class ResourcesTest { + + @Rule public final ExpectedException exception = ExpectedException.none(); + + @Mock private File mockFile; + + @Test + public void getResourceAsTempFile_deleteOnExit() throws IOException { + Resources.getResourceAsTempFile("some_resource.txt", mockFile, new ByteArrayOutputStream()); + + verify(mockFile).deleteOnExit(); + } + + @Test + public void getResourceAsTempFile_contents() throws IOException { + File file = Resources.getResourceAsTempFile("some_resource.txt"); + + assertThat(Files.toString(file, Charsets.UTF_8)).isEqualTo("A resource!"); + } + + @Test + public void getResourceAsTempFile_empty() throws IOException { + exception.expect(IllegalArgumentException.class); + + Resources.getResourceAsTempFile(""); + } + + @Test + public void getResourceAsTempFile_Missing() throws IOException { + exception.expect(FileNotFoundException.class); + + Resources.getResourceAsTempFile("missing_resource.txt"); + } + + @Test + public void getResourceAsTempFile_WriteFailure() throws IOException { + OutputStream badOutputStream = + new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException("denied"); + } + }; + + exception.expect(IOException.class); + exception.expectMessage("denied"); + + Resources.getResourceAsTempFile("some_resource.txt", mockFile, badOutputStream); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java new file mode 100644 index 00000000..4ed7120f --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.bootstrap; + +import static org.mockito.Mockito.mock; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link ContextTrampoline}. */ +@RunWith(MockitoJUnitRunner.class) +public class ContextTrampolineTest { + + private static final ContextStrategy mockContextStrategy; + + static { + mockContextStrategy = mock(ContextStrategy.class); + ContextTrampoline.setContextStrategy(mockContextStrategy); + } + + @Rule public final ExpectedException exception = ExpectedException.none(); + + @Mock private Runnable runnable; + + @Mock private Thread thread; + + @Test + public void setContextStrategy_already_initialized() { + exception.expect(IllegalStateException.class); + + ContextTrampoline.setContextStrategy(mockContextStrategy); + } + + @Test + public void wrapInCurrentContext() { + ContextTrampoline.wrapInCurrentContext(runnable); + + Mockito.verify(mockContextStrategy).wrapInCurrentContext(runnable); + } + + @Test + public void saveContextForThread() { + ContextTrampoline.saveContextForThread(thread); + + Mockito.verify(mockContextStrategy).saveContextForThread(thread); + } + + @Test + public void attachContextForThread() { + ContextTrampoline.attachContextForThread(thread); + + Mockito.verify(mockContextStrategy).attachContextForThread(thread); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java new file mode 100644 index 00000000..f1ca3500 --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.bootstrap; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; + +import java.io.Closeable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link TraceTrampoline}. */ +@RunWith(MockitoJUnitRunner.class) +public class TraceTrampolineTest { + + private static final TraceStrategy mockTraceStrategy = mock(TraceStrategy.class); + + static { + TraceTrampoline.setTraceStrategy(mockTraceStrategy); + } + + @Rule public final ExpectedException exception = ExpectedException.none(); + + @Test + public void setTraceStrategy_already_initialized() { + exception.expect(IllegalStateException.class); + + TraceTrampoline.setTraceStrategy(mockTraceStrategy); + } + + @Test + @SuppressWarnings("MustBeClosedChecker") + public void startScopedSpan() { + Closeable mockCloseable = mock(Closeable.class); + Mockito.when(mockTraceStrategy.startScopedSpan("test")).thenReturn(mockCloseable); + + Closeable closeable = TraceTrampoline.startScopedSpan("test"); + + Mockito.verify(mockTraceStrategy).startScopedSpan("test"); + assertThat(closeable).isSameAs(mockCloseable); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java new file mode 100644 index 00000000..75d8940e --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import com.typesafe.config.ConfigFactory; +import io.opencensus.contrib.agent.Settings; +import net.bytebuddy.agent.builder.AgentBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link ExecutorInstrumentation}. */ +@RunWith(MockitoJUnitRunner.class) +public class ExecutorInstrumentationTest { + + private final ExecutorInstrumentation instrumentation = new ExecutorInstrumentation(); + + private final AgentBuilder agentBuilder = new AgentBuilder.Default(); + + private static final String FEATURE = "context-propagation.executor"; + + @Test + public void instrument_disabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = false")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isSameAs(agentBuilder); + } + + @Test + public void instrument_enabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = true")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isNotSameAs(agentBuilder); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java new file mode 100644 index 00000000..4585c37d --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import com.typesafe.config.ConfigFactory; +import io.opencensus.contrib.agent.Settings; +import net.bytebuddy.agent.builder.AgentBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link ThreadInstrumentation}. */ +@RunWith(MockitoJUnitRunner.class) +public class ThreadInstrumentationTest { + + private final ThreadInstrumentation instrumentation = new ThreadInstrumentation(); + + private final AgentBuilder agentBuilder = new AgentBuilder.Default(); + + private static final String FEATURE = "context-propagation.thread"; + + @Test + public void instrument_disabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = false")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isSameAs(agentBuilder); + } + + @Test + public void instrument_enabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = true")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isNotSameAs(agentBuilder); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java new file mode 100644 index 00000000..3fa1249c --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import com.typesafe.config.ConfigFactory; +import io.opencensus.contrib.agent.Settings; +import net.bytebuddy.agent.builder.AgentBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link UrlInstrumentation}. */ +@RunWith(MockitoJUnitRunner.class) +public class UrlInstrumentationTest { + + private final UrlInstrumentation instrumentation = new UrlInstrumentation(); + + private final AgentBuilder agentBuilder = new AgentBuilder.Default(); + + private static final String FEATURE = "trace.java.net.URL.getContent"; + + @Test + public void instrument_disabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = false")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isSameAs(agentBuilder); + } + + @Test + public void instrument_enabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = true")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isNotSameAs(agentBuilder); + } +} diff --git a/contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt b/contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt new file mode 100644 index 00000000..07319bbd --- /dev/null +++ b/contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt @@ -0,0 +1 @@ +A resource!
\ No newline at end of file diff --git a/contrib/appengine_standard_util/README.md b/contrib/appengine_standard_util/README.md new file mode 100644 index 00000000..3ff5a0ad --- /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 00000000..a5c122a6 --- /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 00000000..9fac951c --- /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 00000000..35e2e087 --- /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 00000000..dc53d8f3 --- /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 00000000..0010d005 --- /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 00000000..7da41cbe --- /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 00000000..d9231837 --- /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 00000000..372e5c60 --- /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 00000000..2b41e9b8 --- /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 00000000..4dd27f29 --- /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 00000000..1c9d62df --- /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 00000000..9404b877 --- /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 00000000..7eb2116b --- /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 00000000..766f2c43 --- /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 00000000..b80cee23 --- /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 00000000..a2de78d0 --- /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 00000000..c09cfbfe --- /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 00000000..fbe1d58f --- /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 00000000..ef06ba2b --- /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 00000000..107f0fea --- /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 00000000..6f8b5165 --- /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 00000000..a908629f --- /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 00000000..7c5c7b99 --- /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 00000000..ecc347d3 --- /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 00000000..92b36d44 --- /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 00000000..a6b5e87c --- /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 00000000..9678fcb7 --- /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 00000000..a3c9f260 --- /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 00000000..77faa9f9 --- /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> + * <TRACE_ID>/<SPAN_ID>[;o=<TRACE_OPTIONS>] + * </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 00000000..fd73b8a9 --- /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 00000000..779be8d8 --- /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: <TRACE_ID>/<SPAN_ID>[;o=<TRACE_TRUE>]". 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 00000000..54ad20ce --- /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 00000000..9e3b984e --- /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 00000000..4492a402 --- /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 00000000..dd1c20fb --- /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 00000000..f84a4da0 --- /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 00000000..d008348e --- /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 00000000..8adf0a5b --- /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 00000000..a5bf1449 --- /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 00000000..4a4a6ebc --- /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 00000000..dd32e448 --- /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 00000000..38b18826 --- /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 00000000..93ad85e0 --- /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 00000000..355c9b67 --- /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 00000000..1205924e --- /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 00000000..bbce4135 --- /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 00000000..3b704058 --- /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 00000000..7af46064 --- /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 00000000..8d99ff20 --- /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 00000000..4d8a2985 --- /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 00000000..5c3e21ff --- /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 00000000..c116f09a --- /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 00000000..9d3c754c --- /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 00000000..1e25c7cf --- /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 00000000..03b0bd4d --- /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 00000000..c09d1c65 --- /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 00000000..c828906d --- /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 00000000..8ff0ff98 --- /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 00000000..f2816676 --- /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 00000000..77d98493 --- /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 00000000..0defcbd7 --- /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 00000000..01927a2d --- /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 00000000..8c740297 --- /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 00000000..941afcce --- /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 00000000..2edc57c7 --- /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 00000000..0fbd7159 --- /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 00000000..218854b1 --- /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 00000000..51f7311c --- /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 00000000..3e4415cf --- /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 00000000..87cb94fd --- /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 00000000..729c2e6c --- /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 00000000..3345783b --- /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 00000000..53ff1c04 --- /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 00000000..de4201fe --- /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 00000000..5cd0e57c --- /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 00000000..eeacfcb7 --- /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 00000000..db6a3555 --- /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 00000000..bba9bab2 --- /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 00000000..e69de29b --- /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 00000000..5e654514 --- /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 00000000..997ed4f4 --- /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 00000000..a4a04f97 --- /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 00000000..924c06ee --- /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 00000000..2a535cec --- /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 00000000..9648d64e --- /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 Binary files differnew file mode 100644 index 00000000..9d303fb1 --- /dev/null +++ b/contrib/zpages/screenshots/rpcz-example.png diff --git a/contrib/zpages/screenshots/statsz-example-1.png b/contrib/zpages/screenshots/statsz-example-1.png Binary files differnew file mode 100644 index 00000000..503a05b2 --- /dev/null +++ b/contrib/zpages/screenshots/statsz-example-1.png diff --git a/contrib/zpages/screenshots/statsz-example-2.png b/contrib/zpages/screenshots/statsz-example-2.png Binary files differnew file mode 100644 index 00000000..bb1229c8 --- /dev/null +++ b/contrib/zpages/screenshots/statsz-example-2.png diff --git a/contrib/zpages/screenshots/traceconfigz-example.png b/contrib/zpages/screenshots/traceconfigz-example.png Binary files differnew file mode 100644 index 00000000..54287683 --- /dev/null +++ b/contrib/zpages/screenshots/traceconfigz-example.png diff --git a/contrib/zpages/screenshots/tracez-example.png b/contrib/zpages/screenshots/tracez-example.png Binary files differnew file mode 100644 index 00000000..cfcf0f3c --- /dev/null +++ b/contrib/zpages/screenshots/tracez-example.png 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 00000000..4d79fb0c --- /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> </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 00000000..00c72d6c --- /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 </td>", measure.getDescription()); + formatter.format("<td class=\"borderLL\">%s </td>", measure.getUnit()); + String measureType = + measure.match( + Functions.returnConstant("Double"), + Functions.returnConstant("Long"), + Functions.throwAssertionError()); + formatter.format("<td class=\"borderLL\">%s </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 00000000..015b83df --- /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 00000000..2a02cca6 --- /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 00000000..f6a36996 --- /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\"> </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\"> </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\"> </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\"> </td>"); + out.write("<td colspan=1 class=\"head\"><b>Running</b></td>\n"); + out.write("<td class=\"borderLW\"> </td>"); + out.write("<td colspan=9 class=\"head\"><b>Latency Samples</b></td>\n"); + out.write("<td class=\"borderRW\"> </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\"> </td>"); + out.write("<td colspan=1></td>\n"); + out.write("<td class=\"borderLW\"> </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\"> </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 00000000..172bca00 --- /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 00000000..710e9a20 --- /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 00000000..975bdfcb --- /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 00000000..2a75fe8b --- /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 00000000..81e64a64 --- /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 00000000..63ea8c45 --- /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 00000000..a7bbf11c --- /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 00000000..7ac5ba65 --- /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 00000000..a93cefd0 --- /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 00000000..921691b7 --- /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 00000000..a065f962 --- /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 00000000..22889e10 --- /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 Binary files differnew file mode 100644 index 00000000..758de960 --- /dev/null +++ b/examples/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a95009c3 --- /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 00000000..cccdd3d5 --- /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 00000000..e95643d6 --- /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 00000000..ce382cd2 --- /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 00000000..5f083126 --- /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 00000000..310e652f --- /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 00000000..30e41633 --- /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 00000000..15a0a896 --- /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 00000000..55d6c225 --- /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 00000000..c71e0f3e --- /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 00000000..727c5fbb --- /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 00000000..c8df144f --- /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 00000000..5cfc9dfc --- /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 00000000..fae4e3ff --- /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 00000000..9f0338af --- /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 00000000..282b40ec --- /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 00000000..1bd79300 --- /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 00000000..fa19efc9 --- /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 00000000..fe8563c4 --- /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 00000000..288813d3 --- /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 00000000..d555c92b --- /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 00000000..3e8b95ed --- /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 00000000..ca8315b9 --- /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 00000000..3bd98451 --- /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 00000000..7c61f896 --- /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 00000000..d496b1e5 --- /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 00000000..5601a54c --- /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 00000000..2eb75c4c --- /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 00000000..e8b4d756 --- /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 00000000..f7915b71 --- /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 00000000..348778e2 --- /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 00000000..34f4dfa7 --- /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 00000000..1d3508fb --- /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 00000000..cc5730b1 --- /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 00000000..d8852d5f --- /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 00000000..1b35c635 --- /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 00000000..0bc302a6 --- /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 00000000..4f8715b0 --- /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 00000000..5ffed9d5 --- /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 00000000..c4008ca1 --- /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 00000000..51c54916 --- /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 00000000..cd536e8f --- /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 00000000..27593829 --- /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 00000000..2d5eba1b --- /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 00000000..f5e3edd5 --- /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 00000000..22ace227 --- /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 00000000..028bc208 --- /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 00000000..649a026f --- /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 00000000..da2ce354 --- /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 00000000..3b5e119e --- /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 00000000..a4d03df3 --- /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 00000000..7a5b68eb --- /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 00000000..04829aa4 --- /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 00000000..e0a16296 --- /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 00000000..4890f01a --- /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 00000000..9d6a7976 --- /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 00000000..f918f015 --- /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 00000000..c00b0133 --- /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 00000000..51f2566d --- /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 00000000..a7fb0ff6 --- /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 00000000..46f01ffc --- /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 00000000..9267e201 --- /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 00000000..c2b77e4e --- /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 00000000..4f25bd6e --- /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 00000000..777c08d0 --- /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 00000000..65729803 --- /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 00000000..5c468ded --- /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 00000000..c7bf1e95 --- /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 00000000..5edc06df --- /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 00000000..ec778ba6 --- /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 00000000..d01dd7eb --- /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 00000000..fbdb35e3 --- /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 00000000..f619021b --- /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 00000000..813066bc --- /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 00000000..81bc5c60 --- /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 00000000..c58acdb1 --- /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 00000000..74c7c29e --- /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 00000000..9186a47c --- /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 00000000..83dc970e --- /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 00000000..8797cc77 --- /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 00000000..f78832d0 --- /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 00000000..0182ae94 --- /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 00000000..de022c3f --- /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 00000000..6926e869 --- /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 00000000..6a12a899 --- /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 00000000..32458597 --- /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 00000000..8b28dc06 --- /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 00000000..4398360d --- /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 00000000..530dff7d --- /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 00000000..e20360e8 --- /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 00000000..70bc725c --- /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 00000000..aad5a563 --- /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 00000000..7e293003 --- /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 00000000..2a032d0f --- /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 00000000..014f9a9b --- /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 Binary files differnew file mode 100644 index 00000000..758de960 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a95009c3 --- /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 00000000..cccdd3d5 --- /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 00000000..e95643d6 --- /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 00000000..3dee26f9 --- /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 00000000..6dacdddb --- /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 00000000..a0445b53 --- /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; + * } + * + * @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 00000000..53c354f1 --- /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 00000000..6b9fe69a --- /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 00000000..8dd1f373 --- /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 00000000..1cd70023 --- /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 00000000..d13e3982 --- /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 00000000..76da3bd0 --- /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 00000000..f12498fa --- /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 00000000..439933de --- /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 00000000..23606a48 --- /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 00000000..e94cf254 --- /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 00000000..e58ce1cb --- /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 00000000..901177c8 --- /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 00000000..21158c36 --- /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 00000000..736c3705 --- /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 00000000..70e590bf --- /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 00000000..14636920 --- /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 00000000..98626926 --- /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 00000000..f08289cf --- /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 00000000..d7b1b112 --- /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 00000000..2baa5000 --- /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 00000000..6eb1149a --- /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 00000000..51efe894 --- /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 00000000..58c61c89 --- /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 00000000..c70f5860 --- /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 00000000..05a039b9 --- /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 00000000..944f62fd --- /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 00000000..b7104c9b --- /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 00000000..90e3e706 --- /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 00000000..c314e980 --- /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 00000000..3460d7a4 --- /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 00000000..f5a8dc8f --- /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 00000000..1a301ecf --- /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 00000000..1aef6727 --- /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 00000000..173c3aec --- /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 00000000..6f585a10 --- /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 00000000..172db539 --- /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 00000000..ee51796c --- /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 00000000..d867b342 --- /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 00000000..5da0cad8 --- /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 00000000..7bf92572 --- /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 00000000..0dfb1d26 --- /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 00000000..6e2bff1c --- /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 00000000..928675e9 --- /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 00000000..fbb593f5 --- /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 00000000..741b399b --- /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 00000000..17e99d46 --- /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 00000000..f9ebea41 --- /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 00000000..20ea97f8 --- /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 00000000..e6bb12f5 --- /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 00000000..eae54c5d --- /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 00000000..a17198d8 --- /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 00000000..f7a8ff82 --- /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 00000000..5fbc5050 --- /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 00000000..dcf9a1b7 --- /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 00000000..88c31bae --- /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 00000000..2daad95e --- /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 00000000..5a25da5b --- /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 00000000..9ba0da40 --- /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 00000000..8a5f8e05 --- /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 00000000..af3545bc --- /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 00000000..5565e9de --- /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 00000000..6adaa200 --- /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 00000000..c1432432 --- /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 00000000..48df8055 --- /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 00000000..25f0c613 --- /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 00000000..19817380 --- /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 00000000..f7aeac71 --- /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 00000000..0d8e493b --- /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 00000000..962f5b01 --- /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 00000000..e67c2f8e --- /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 00000000..51a7b05c --- /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 00000000..22d8e41a --- /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 00000000..70be5a90 --- /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 00000000..d928d93c --- /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 00000000..233fbd31 --- /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: <version_id><version_format> + * <li>version_id: 1-byte representing the version id. + * <li>For version_id = 0: + * <ul> + * <li>version_format: <field><field> + * <li>field_format: <field_id><field_format> + * <li>Fields: + * <ul> + * <li>TraceId: (field_id = 0, len = 16, default = "0000000000000000") - + * 16-byte array representing the trace_id. + * <li>SpanId: (field_id = 1, len = 8, default = "00000000") - 8-byte array + * representing the span_id. + * <li>TraceOptions: (field_id = 2, len = 1, default = "0") - 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 00000000..f608543d --- /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 00000000..b7e6a93a --- /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 00000000..32a3e687 --- /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 00000000..2e0bde21 --- /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 00000000..e69a284f --- /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 00000000..ec9cad6c --- /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 00000000..b0899084 --- /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 00000000..e83bb642 --- /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 00000000..68bfda31 --- /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 00000000..7f8515d3 --- /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 00000000..fb91641c --- /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 00000000..e549dadb --- /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 00000000..39a53e1a --- /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 00000000..19e8a6c5 --- /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 00000000..25f33a94 --- /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 00000000..66e971f6 --- /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 00000000..a6139e53 --- /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 00000000..06f50fed --- /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 00000000..1e22a7a1 --- /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 00000000..04861df9 --- /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 00000000..bd8b5b88 --- /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 00000000..ea1bf346 --- /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 00000000..a4018b79 --- /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 00000000..1a14ac6e --- /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 00000000..6a8fe4c7 --- /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 00000000..1859e081 --- /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 00000000..4ca2ae76 --- /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 00000000..1bc13c59 --- /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 00000000..dcfba508 --- /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 00000000..26a072f6 --- /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 00000000..8db0e389 --- /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 00000000..1b1aa042 --- /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 00000000..ed68fe3d --- /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 00000000..c576860d --- /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 00000000..b293a225 --- /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 00000000..3267eac5 --- /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 00000000..9f468442 --- /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 00000000..d10be6a2 --- /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 00000000..ecaeda6d --- /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 00000000..4b8993ff --- /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 00000000..68ce1c18 --- /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 00000000..7d8b434e --- /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 00000000..96669df7 --- /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 00000000..b9fbd432 --- /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 00000000..f8f1d917 --- /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 00000000..d7ac2ae8 --- /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 00000000..52e6bb3c --- /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 00000000..f43be479 --- /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 00000000..00ed90fe --- /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 00000000..ad7bb9b1 --- /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 00000000..b8692fdf --- /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 00000000..6161c12a --- /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 00000000..a58a9d3e --- /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 00000000..dc0d900c --- /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 00000000..8c067557 --- /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 00000000..5e80b93a --- /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 00000000..7ee900a6 --- /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 00000000..313f8916 --- /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 00000000..890cdb15 --- /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 00000000..c4a609a4 --- /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 00000000..df13e423 --- /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 00000000..7b7bec50 --- /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 00000000..7c224edf --- /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 00000000..268bbf3d --- /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 00000000..811b0597 --- /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 00000000..b670cb7f --- /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 00000000..6d73aff1 --- /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 00000000..24cd7fcd --- /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)); + } +} |